Skip to content

20. Exercício prático – versão 10

A versão anterior demonstrou que os dados fiscais, partilhados por todos os utilizadores da aplicação, devem ser armazenados numa memória de âmbito [Application]. Vamos utilizar um servidor Redis [https://redis.io] para implementar esta memória.

20.1. Redis

A memória de alcance [Application] será implementada por um servidor Redis. Os scripts PHP que necessitem desta memória de aplicação serão clientes deste servidor:

Image

20.2. Instalação do Redis

O Laragon vem com um servidor Redis desativado por predefinição. Por isso, é necessário começar por ativá-lo:

Image

  • em [3], ative o servidor [Redis];
  • em [4], manter a porta [6379] que os clientes Redis utilizam por predefinição;

Os serviços Laragon são reiniciados automaticamente após a ativação do Redis:

Image

20.3. O cliente Redis no modo de comando

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

Image

  • em [1], o comando [redis-cli] inicia o cliente no modo de comando do servidor Redis;

Em julho de 2019, o cliente Redis pode utilizar 172 comandos para comunicar com o servidor [https://redis.io/commands#list]. Uma delas, [command count] [2], apresenta este número [3].

Vamos apresentar apenas aqueles de que vamos precisar na nossa aplicação PHP. Vamos utilizar o Redis para um único fim: armazenar um array [‘attribut’=>’valeur’] na memória do Redis. Isto é feito com o comando Redis [set attribut valeur] [4]. O valor pode, em seguida, ser recuperado com o comando [get attribut] [5]. É tudo o que vamos precisar.

Pode ser necessário esvaziar a memória do Redis. Isto é feito com o comando [flushdb] [6]. Em seguida, se solicitarmos o valor do atributo [titre] [7], obtemos uma referência [nil] [8] indicando que o atributo não foi encontrado. Também é possível utilizar 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, temos de instalar um cliente Redis para o PHP:

Image

Existem várias bibliotecas que implementam um cliente Redis. Iremos utilizar a biblioteca [Predis] [https://github.com/nrk/predis] (julho de 2019). Tal como as anteriores, esta instala-se com o [composer] num terminal Laragon:

Image

20.5. Código do servidor

Image

O ficheiro 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, para além do script [impots-server.php]. Utiliza elementos das versões 08 e 09;
  • linha 19: uma dependência necessária da biblioteca [predis] que acabámos de instalar;

O código do servidor [impots-server.php] evolui da seguinte forma:


<?php

// respeito rigoroso dos 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 ficheiro de configuração
define("CONFIG_FILENAME", "Data/config-server.json");
// alias da classe
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

// sessão
$session = new Session();
$session->start();


// 1.º registo
$logger->write("\n---nouvelle requête\n");

// recuperar a solicitação atual
$request = Request::createFromGlobals();
// autenticação apenas na primeira vez
if (!$session->has("user")) {

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

// temos um utilizador 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 {
  // registos
  $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();
  // ligamo-nos ao servidor para verificar se 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 da base 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 alcance [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 âmbito [application]
  $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
  $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
  // instanciação da camada [dao]
  $dao = new ServerDaoWithRedis(NULL, $taxAdminData);
  // registos
  $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);
// apresenta-se a resposta
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 da aplicação
  // $predisClient: um cliente [predis]
  //
  //: é enviado um e-mail ao administrador
  // SendAdminMail intercepta todas as exceções e regista-as ele próprio
  $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 de envio da 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: estado HTTP da resposta
  // $headers: cabeçalhos HTTP a incluir na resposta
  // $logger: o logger da aplicação
  // $predisClient: um cliente [predis]
  //
  // estado 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();
  // registo
  if ($logger != NULL) {
    $logger->write("$body\n");
    $logger->close();
  }
  // encerramento da ligação [redis]
  if ($predisClient != NULL) {
    $predisClient->disconnect();
  }
}

Comentários

  • linha 15: atribui-se o alias [ServerDaoWithRedis] à classe [\Application\ServerDaoWithSession] para refletir a alteração na implementação do script do servidor;
  • linhas 18-19: a sessão é mantida. Temos aqui duas informações a reter:
    • o facto de o utilizador se ter autenticado corretamente. Esta informação tem o âmbito [session]: está associada a um utilizador específico e não é válida para outros utilizadores;
    • os dados da administração fiscal. Esta informação tem o âmbito [application]: não está associada a um utilizador específico, mas é válida para todos os utilizadores;
  • linhas 54-64: criação do cliente [redis] que irá comunicar com o servidor [redis]. Este cliente irá comunicar com a porta predefinida do servidor. Se este não comunicasse na sua porta predefinida ou se não estivesse na máquina [localhost], seria necessário passar estas informações ao construtor da classe [\Predis\Client];
  • linha 59: ligamos imediatamente o cliente ao servidor para verificar se este responde;
  • linhas 60-65: se a ligação ao servidor Redis falhar, envia-se uma resposta de erro ao cliente e será enviado um e-mail ao administrador da aplicação;
  • linha 67: solicita-se ao servidor [redis] a chave [taxAdminData]. Se esta não for encontrada, os dados fiscais são obtidos da base de dados (linha 72);
  • linha 75: a chave [taxAdminData] é colocada na memória [redis] associada à cadeia jSON da variável [$taxAdminData], que é um objeto do tipo [TaxAdminData]. O método [$redis→set] espera uma cadeia de caracteres como valor da chave. Por conseguinte, irá tentar converter o objeto do tipo [TaxAdminData] para o tipo [string]. É então, implicitamente, o método [TaxAdminData->__toString] que será chamado. Este produz a cadeia jSON do objeto [TaxAdminData];
  • linha 84: a chave [taxAdminData] encontra-se na memória [redis], pelo que se recupera o seu valor. Sabe-se que se trata da cadeia jSON de um objeto [TaxAdminData]. Em seguida, descodifica-se esta cadeia para obter um tabuleiro de atributos;
  • linha 85: a partir desta matriz, é instanciado um novo objeto [TaxAdminData];
  • linha 87: a camada [dao] é instanciada;

20.6. Código do cliente

Image

A versão 10 do cliente é idêntica à versão 9. A única alteração diz respeito ao ficheiro 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 que na versão 09. Vamos simplesmente testar um novo caso de erro:

Image

O resultado na consola é 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

Image

A classe de teste [ClientMetierTest] da versão 10 é idêntica à da versão 09, com uma única exceção:


<?php

// respeito rigoroso dos 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 eliminar, utilizando o cliente [redis-cli], a chave [taxAdminData] da memória do servidor [redis]:

Image

Agora, vamos executar o teste:

Image

Agora, vamos analisar os registos [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á foi referido que, em cada teste, o construtor da classe de teste é reexecutado, o que faz com que a classe [ClientDao] testada seja instanciada em cada teste com um cookie de sessão inexistente. Tudo acontece, portanto, como se os 11 testes representassem 11 utilizadores diferentes, com 11 sessões diferentes.

  • linha 6: os dados fiscais são obtidos da base de dados;
  • linhas 13 e 20: os dados fiscais são obtidos da memória [redis]. Trata-se, portanto, de uma memória com âmbito [application] partilhada por todos os utilizadores da aplicação;

20.8. Interface web do servidor [Redis]

Vimos que o servidor [Redis] pode ser gerido em modo de comando. Também pode ser gerido através de uma interface web:

Image

  • em [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]:

Image

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