Skip to content

19. Exercício prático – versão 9

Nesta versão, vamos aprimorar o servidor da seguinte maneira:

  • atualmente, a cada solicitação, os dados da administração fiscal são buscados no banco de dados. Vamos utilizar uma sessão:
    • na primeira solicitação de um usuário, os dados da administração fiscal são buscados no banco de dados e armazenados na sessão;
    • nas solicitações seguintes do mesmo usuário, os dados da administração fiscal são buscados na sessão. É possível esperar uma ligeira redução no tempo de execução, pois as consultas ao banco de dados são dispendiosas;
  • o servidor registrará em um arquivo de texto os momentos importantes:
    • a autenticação bem-sucedida ou malsucedida;
    • a validade ou não dos parâmetros enviados pelo cliente;
    • o resultado do cálculo do imposto;
    • os diversos casos de erro;
  • em caso de erro fatal, um e-mail será enviado ao administrador do aplicativo;

O cliente também deverá ser modificado para gerenciar o cookie de sessão que será enviado a ele.

19.1. O servidor

Vamos nos concentrar na parte do servidor do aplicativo.

Image

Essa arquitetura será implementada pelos seguintes scripts:

Image

19.1.1. Utilitários

Image

19.1.1.1. A classe [Logger]

A classe [Logger] será utilizada para gravar registros em um arquivo de texto:


<?php

namespace Application;

class Logger {
  // atributo
  private $resource;

  // construtor
  public function __construct(string $logsFilename) {
    // abertura do arquivo
    $this->resource = fopen($logsFilename, "a");
    if (!$this->resource) {
      throw new ExceptionImpots("Echec lors de la création du fichier de logs [$logsFilename]");
    }
  }

  // gravação de uma mensagem nos logs
  public function write(string $message) {
    fputs($this->resource, (new \DateTime())->format("d/m/y H:i:s:v") . " : $message");
  }

  // fechamento do arquivo de logs
  public function close() {
    fclose($this->resource);
  }

}

Comentários

  • linha 7: o recurso do arquivo de logs;
  • linha 10: o construtor da classe recebe como parâmetro o nome do arquivo de logs;
  • linha 12: abre-se o arquivo de texto no modo de adição (a+): o arquivo será aberto e seu conteúdo preservado. As gravações serão feitas após o conteúdo atual;
  • linhas 13-15: se não for possível abrir o arquivo, é lançada uma exceção;
  • linhas 19-21: o método [write] permite gravar a mensagem [$message] no arquivo de logs, precedida da data e da hora;
  • linhas 24-16: o método [close] permite fechar o arquivo de logs;

Nota: o aplicativo de servidor pode atender a vários clientes simultaneamente. No entanto, há apenas um arquivo de logs para todos. Portanto, existe o risco de acessos simultâneos para gravar no arquivo. Seria necessário, assim, sincronizar as gravações para evitar que elas se misturem. Para isso, o PHP utiliza os semáforos [https://www.php.net/manual/fr/book.sem.php]. Ignoraremos a sincronização das gravações neste momento, mas é preciso estar ciente do problema.

19.1.1.2. A classe [SendAdminMail]

A classe [SendAdminMail] permite enviar um e-mail ao administrador do aplicativo em caso de falha do mesmo:


<?php

namespace Application;

class SendAdminMail {
  // atributos
  private $config;
  private $logger;

  // construtor
  public function __construct(array $config, Logger $logger = NULL) {
    $this->config = $config;
    $this->logger = $logger;
  }

  public function send() {
    // envia $this->config['message'] para o servidor SMTP $this->config['smtp-server'] na porta $infos[smt-port]
    // se $this->config['tls'] for verdadeiro, será utilizado o suporte TLS
    // o e-mail é enviado em nome de $this->config['from']
    // para o destinatário $this->config['to']
    // a mensagem tem como assunto $this->config['subject']
    // os anexos de $this->config['attachments'] são anexados ao e-mail
    // o resultado do método
    try {
      // criação da mensagem
      $message = (new \Swift_Message())
        // assunto da mensagem
        ->setSubject($this->config["subject"])
        // remetente
        ->setFrom($this->config["from"])
        // destinatários com um dicionário (setTo/setCc/setBcc)
        ->setTo($this->config["to"])
        // texto da mensagem
        ->setBody($this->config["message"])
      ;
      // anexos
      foreach ($this->config["attachments"] as $attachment) {
        // caminho do anexo
        $fileName = __DIR__ . $attachment;
        // verifica-se se o arquivo existe
        if (file_exists($fileName)) {
          // anexamos o documento à mensagem
          $message->attach(\Swift_Attachment::fromPath($fileName));
        } else {
          if ($this->logger !== NULL) {
            // erro
            $this->logger->write("L'attachement [$fileName] n'existe pas\n");
          }
        }
      }
      // protocolo 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 {
        // sem TLS
        $transport = (new \Swift_SmtpTransport($this->config["smtp-server"], $this->config["smtp-port"]));
      }
      // o gerenciador de envio
      $mailer = new \Swift_Mailer($transport);
      // envio da mensagem
      $mailer->send($message);
      // fim
      if ($this->logger !== NULL) {
        $this->logger->write("Message [{$this->config["message"]}] envoyé à {$this->config["to"]}\n");
      }
    } catch (\Throwable $ex) {
      // erro
      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 o envio do e-mail;
    • [$logger]: um logger que permite registrar os momentos importantes do envio do e-mail;

O array associativo terá o seguinte formato:

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": []
}
  • linhas 16-76: o método [send] permite enviar o e-mail. Esse código foi apresentado e descrito no parágrafo com o link;

19.1.2. A camada [dao]

Image

O script [ServeurDaoWithSession.php] é o seguinte:


<?php

// espaço de nomes
namespace Application;

// definição de uma classe ImpotsWithDataInDatabase
class ServerDaoWithSession extends ServerDao {

  // construtor
  public function __construct(string $databaseFilename = NULL, TaxAdminData $taxAdminData = NULL) {
    // caso mais simples
    if ($taxAdminData !== NULL) {
      $this->taxAdminData = $taxAdminData;
    } else {
      // passamos o controle para a classe pai
      parent::__construct($databaseFilename);
    }
  }

}

Comentários

  • linha 7: a classe [ServerDaoWithSession] da versão 09 estende a classe [ServerDao] da versão 08. De fato, a classe [ServerDao] sabe utilizar o banco de dados. Resta-nos apenas considerar o caso em que os dados da administração fiscal já tenham sido obtidos:
  • linha 10: o construtor agora recebe dois parâmetros:
    • [string $databaseFilename]: nome do arquivo que contém as informações necessárias para se conectar ao banco de dados caso os dados da administração fiscal ainda não tenham sido obtidos; NULL, caso contrário;
    • [TaxAdminData $taxAdminData]: os dados da administração fiscal, caso já tenham sido obtidos; NULL, caso contrário;

Ao iniciar uma sessão na web, a camada [dao] será construída com um objeto [$databaseFilename] que não seja NULL e um objeto [taxAdminData] NULL. Os dados da administração fiscal serão então consultados no banco de dados e armazenados na sessão. Em consultas posteriores da mesma sessão, a camada [dao] será construída com um objeto [databaseFilename], NULL e um objeto [taxAdminData] provenientes da sessão, e não com o NULL. Portanto, não haverá consulta ao banco de dados.

19.1.3. O script do servidor

O script do servidor [impots-server.php] é configurado pelo seguinte arquivo 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] evolui da seguinte forma:


<?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");

// recuperação da configuração
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// inclui-se as dependências necessárias ao script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// dependências absolutas (bibliotecas de terceiros)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;

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

// preparação da resposta JSON do servidor
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");

// criação do arquivo de logs
try {
  $logger = new Logger($config['logsFilename']);
} catch (ExceptionImpots $ex) {
  // erro interno do servidor
  doInternalServerError($ex->getMessage(), $response, NULL, $config['adminMail']);
  // concluído
  exit;
}

// primeiro registro
$logger->write("\n---nouvelle requête\n");

// recuperando a solicitação atual
$request = Request::createFromGlobals();
// autenticação apenas na primeira vez
if (!$session->has("user")) {
  // registro
  $logger->write("Autentification en cours…\n");
  // autenticação

  }
  // o usuário foi encontrado?
  if (!$trouvé) {
    // não encontrado – código 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
    );
    // concluído
    exit;
  } else {
    // registramos na sessão que o usuário foi autenticado
    $session->set("user", TRUE);
    // registro
    $logger->write("Authentification réussie [$requestUser, $requestPassword]\n");
  }
} else {
  // registro
  $logger->write("Authentification prise en session…\n");
}
// temos um usuário válido — verificamos os parâmetros recebidos
$erreurs = [];
// devemos ter três parâmetros GET


// erros?
if ($erreurs) {
// envia-se 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
// criação da camada [dao]
if (!$session->has("taxAdminData")) {
  // os dados são extraídos do banco de dados
  $logger->write("données fiscales prises en base de données\n");
  try {
    // construção da camada [dao]
    $dao = new ServerDaoWithSession($config["databaseFilename"], NULL);
    // os dados são colocados na sessão
    $session->set("taxAdminData", $dao->getTaxAdminData());
  } catch (\RuntimeException $ex) {
    // o erro é registrado
    doInternalServerError(utf8_encode($ex->getMessage()), $response, $logger, $config['adminMail']);
    // concluído
    exit;
  }
} else {
  // os dados são obtidos da sessão
  $dao = new ServerDaoWithSession(NULL, $session->get("taxAdminData"));
  // registros
  $logger->write("données fiscales prises en session\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);
// fim
exit;

function doInternalServerError(string $message, Response $response, Logger $logger = NULL, array $infos) {
  // envio de 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 um código de erro 500 ao cliente
  sendResponse($response, ["erreur" => $message], Response::HTTP_INTERNAL_SERVER_ERROR, [], $logger);
}

// função para enviar a resposta HTTP ao cliente
function sendResponse(Response $response, array $result, int $statusCode, array $headers, Logger $logger) {
  // $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
  //
  // 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();
  }
}

Comentários

  • linhas 34-35: inicia-se uma sessão;
  • linhas 38-40: prepara-se uma resposta jSON;
  • linhas 42-50: tenta-se criar o arquivo de logs. 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 ser registrada. Deve ser codificada em UTF-8;
    • [$response]: o objeto [Response] que encapsula a resposta do servidor ao seu cliente;
    • [$logger]: o objeto [Logger] que permite gerar os logs;
    • [$infos]: as informações necessárias para enviar um e-mail ao administrador do aplicativo;
  • linhas 135-137: envia-se um e-mail ao administrador do aplicativo;
  • linha 139: envia-se a resposta ao cliente:
    • $response: resposta HTTP;
    • $result: o servidor envia a sequência jSON da tabela [‘réponse’=>["erreur" => $message]];
    • $statusCode: [Response::HTTP_INTERNAL_SERVER_ERROR], código 500;
    • $headers: [], não há cabeçalhos HTTP a serem adicionados à resposta;
    • $logger: o logger do aplicativo;
  • linha 58: graças à sessão estabelecida, a autenticação do cliente será feita apenas uma vez:
    • uma vez que o cliente estiver autenticado, colocaremos uma chave [user] na sessão (linha 78);
    • na próxima solicitação do mesmo cliente, a linha 58 evita uma autenticação que já se tornou desnecessária;
  • linha 103: graças à sessão estabelecida, a busca de dados no banco de dados será realizada apenas uma vez:
    • na primeira solicitação, será realizada a consulta ao banco de dados (linha 108). Os dados recuperados são então armazenados na sessão (linha 110), associados à chave [taxAdminData];
    • nas consultas seguintes, a chave [taxAdminData] será encontrada na sessão (linha 103) e, então, os dados do disco serão comunicados diretamente à camada [dao] (linha 119);
  • linhas 111-116: a busca pelos dados fiscais no banco de dados pode falhar. Nesse caso, é enviado ao cliente um código [500 Internal Server Error];
  • linha 113: a mensagem de erro da exceção do driver MySQL é codificada como ISO 8859-1. Ela é convertida para UTF-8 para que seja registrada corretamente;
  • o restante do código é praticamente 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 sequência jSON de um array [‘résultat’=>qqChose];
  • linha 156: às vezes, há cabeçalhos HTTP a serem adicionados à resposta. Esse é o caso na linha 71;
  • linha 158: a resposta é enviada;
  • linhas 160-163: a resposta é registrada no log e o registrador é fechado;

19.1.4. Testes [Codeception]

Image

Vamos testar apenas a camada [dao], que é a única que sofreu alterações.

O código do teste [ServerDaoTest] é o seguinte:


<?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:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-09");
// caminho do arquivo de configuração
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");

// recuperação da configuração
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// inclui-se as dependências necessárias ao script
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
  require "$rootDirectory$dependency";
}
// dependências absolutas (bibliotecas de terceiros)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}

// teste -----------------------------------------------------

class ServerDaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    // pai
    parent::__construct();
    // recuperação da configuração
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // criação da camada [dao]
    $dao = new ServerDaoWithSession(ROOT . "/" . $config["databaseFilename"]);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // testes
  public function testTaxAdminData() {

  }

}
  • linhas 9-24: criamos um ambiente de execução idêntico ao do script de servidor [impots-server];
  • linha 38: para construir a camada [dao], instanciamos a classe [ServerDaoWithSession];

O resultado dos testes é o seguinte:

Image

19.2. O cliente

Estamos interessados na parte do cliente do aplicativo.

Image

Essa arquitetura será implementada pelos seguintes scripts:

Image

Na nova versão, apenas mudam:

  • o arquivo de configuração [config-client.json];
  • a camada [dao] do cliente;

19.2.1. A camada [dao]

A camada [Dao] sofre as seguintes alterações:


<?php

namespace Application;

// dependências
use \Symfony\Component\HttpClient\HttpClient;

class ClientDao implements InterfaceClientDao {
  // uso de um Trait
  use TraitDao;
  // atributos
  private $urlServer;
  private $user;
  private $sessionCookie;

  // construtor
  public function __construct(string $urlServer, array $user) {
    $this->urlServer = $urlServer;
    $this->user = $user;
  }

  // cálculo do imposto
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // cookie de sessão?
    if (!$this->sessionCookie) {
      // criação de um cliente HTTP
      $httpClient = HttpClient::create([
          'auth_basic' => [$this->user["login"], $this->user["passwd"]],
          "verify_peer" => false
      ]);
      // envia-se a solicitação ao servidor sem cookie de sessão
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ]
      ]);
    } else {
      // envia-se a solicitação ao servidor com o cookie de sessão
      // cria-se um cliente HTTP
      $httpClient = HttpClient::create([
          "verify_peer" => false
      ]);
      $response = $httpClient->request('GET', $this->urlServer,
        ["query" => [
            "marié" => $marié,
            "enfants" => $enfants,
            "salaire" => $salaire
          ],
          "headers" => ["Cookie" => $this->sessionCookie]
      ]);
    }
    // recupera-se a resposta
    $json = $response->getContent(false);
    $array = \json_decode($json, true);
    $réponse = $array["réponse"];
    // registros
    print "$json=json\n";
    // recupera-se o status da resposta
    $statusCode = $response->getStatusCode();
    // erro?
    if ($statusCode !== 200) {
      // ocorreu um erro — é lançada uma exceção
      $réponse = ["statut HTTP" => $statusCode] + $réponse;
      $message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
      throw new ExceptionImpots($message);
    }
    if (!$this->sessionCookie) {
      // recuperando o cookie de sessão
      $headers = $response->getHeaders();
      if (isset($headers["set-cookie"])) {
        // cookie de sessão?
        foreach ($headers["set-cookie"] as $cookie) {
          $match = [];
          $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
          if ($match) {
            $this->sessionCookie = "PHPSESSID=" . $champs[1];
          }
        }
      }
    }
    // retornamos a resposta
    return $réponse;
  }

}

Comentários

A modificação da camada [dao] consiste agora em gerenciar uma sessão:

  • linha 14: o cookie da sessão;
  • linhas 25-39: na primeira solicitação, esse cookie não existe; então, faz-se a solicitação ao servidor enviando as informações de autenticação (linha 28);
  • linhas 40-53: nas solicitações seguintes, normalmente já se possui o cookie de sessão. Portanto, não se enviam as informações de autenticação (linhas 42-44);
  • linhas 69-82: a resposta do servidor à primeira solicitação conterá um cookie de sessão. Ele é recuperado. Esse código já foi utilizado e explicado no parágrafo com o link;
  • linha 78: o cookie de sessão recuperado é armazenado no atributo da classe [$sessionCookie];

Observação: poderíamos ter mantido a versão anterior da camada [dao] e realizado a autenticação a cada solicitação, pois o custo disso é insignificante. Por uma questão pedagógica, quisemos relembrar como um cliente HTTP poderia gerenciar uma sessão.

19.2.2. O arquivo de configuração

O arquivo de configuração jSON sofre as seguintes alterações:


{
    "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 o URL da linha 24 é alterado.

19.3. Alguns testes

19.3.1. Teste 1

Primeiramente, executamos o cliente em um ambiente sem erros. Os resultados continuam sendo os mesmos das versões anteriores. Mas agora, no lado do servidor, temos um arquivo de logs [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: na primeira solicitação, ocorre a autenticação e a busca de dados no banco;
  • linhas 9-14: na consulta seguinte, não há mais autenticação e os dados são obtidos a partir da sessão. Isso se repete nas consultas seguintes (linhas 15 e seguintes);

19.3.2. Teste 2

Agora, vamos desligar o banco de dados MySQL. No lado do cliente, obtemos o seguinte resultado no console:


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 logs [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 obter o e-mail recebido pelo administrador do aplicativo, utilizamos o script [imap-03.php], conforme descrito no parágrafo, vinculado ao arquivo de configuração [config-imap-01.json] a seguir:

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

Obtém-se o seguinte resultado:

Image

O arquivo [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 fazer com que o arquivo [logs.txt] não possa ser criado. Para isso, basta criar uma pasta chamada [logs.txt]:

Image

Feito isso, vamos executar o cliente.

No lado do cliente, obtemos os seguintes resultados no console:


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 há logs, 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, no arquivo de configuração do cliente, credenciais incorretas para o cliente que está se conectando.

O cliente exibe os seguintes resultados no console:


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

No lado do servidor, aparecem os seguintes logs:


---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 inserir o usuário correto, [admin, admin], no arquivo de configuração do cliente.

Agora, vamos acessar o endereço URL [http://localhost/php7/scripts-web/impots/version-08/impots-server.php] do servidor diretamente em um navegador, sem passar parâmetros:

No arquivo de logs [logs.txt] do servidor, encontramos 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]

Assim como foi feito nas versões anteriores, vamos escrever testes [Codeception] para a versão 09.

Image

19.4.0.1. Teste da camada [métier]

O teste [ClientMetierTest.php] é o seguinte:


<?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-09");

// caminho do arquivo de configuração
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");

// recuperamos a configuração
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);

// inclui-se as dependências necessárias ao script
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
  require "$rootDirectory/$dependency";
}
// dependências absolutas (bibliotecas de terceiros)
foreach ($config["absoluteDependencies"] as $dependency) {
  require "$dependency";
}
//
// usa
use Codeception\Test\Unit;
use const CONFIG_FILENAME;
use const ROOT;

// classe de teste
class ClientMetierTest extends Unit {
  
}

Comentários

  • em relação à classe de teste da versão 08, apenas a linha 10 foi alterada, que especifica a pasta raiz do cliente a ser testado;

Os resultados do teste são os seguintes:

Image

É interessante verificar os logs 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}}

Percebe-se que os dados da administração fiscal são sempre obtidos do banco de dados e nunca da sessão. Voltemos ao código do teste executado:


<?php

// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Application;



// classe de teste
class ClientMetierTest extends Unit {
  // camada de negócios
  private $métier;

  public function __construct() {
    parent::__construct();
    // recuperação da configuração
    $config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
    // criação da camada [dao]
    $clientDao = new ClientDao($config["urlServer"], $config["user"]);
    // criação da camada [métier]
    $this->métier = new ClientMetier($clientDao);
  }

  // testes
  public function test1() {

  }

  public function test2() {

  }

  public function test3() {

  }



}

Em uma classe de teste [Codeception], o construtor é executado para cada teste.

  • linha 21: um novo [ClientDao] é, portanto, criado para cada teste com um cookie de sessão NULL. Isso explica por que esse cliente não se beneficia de nenhuma sessão;

Este exemplo nos mostra que a sessão não é o local adequado para armazenar os dados da administração fiscal. Na verdade, esses dados são comuns a todos os usuários do aplicativo. No entanto, aqui, eles são duplicados em cada uma das sessões desses usuários.

Na programação web, distinguem-se três tipos de visibilidade para os dados compartilhados:

  • dados compartilhados por todos os usuários do aplicativo web. Geralmente, são dados somente para leitura. O PHP não dispõe nativamente dessa memória;
  • dados compartilhados pelas solicitações de um mesmo cliente. Esses dados são armazenados na sessão. Fala-se, então, em “sessão do cliente” para designar a memória do cliente. Todas as solicitações de um cliente têm acesso a essa sessão. Elas podem armazenar e ler informações nela. Nos scripts anteriores, essa 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 usuário pode ser processada por várias ações sucessivas. O contexto da solicitação permite que uma ação 1 transmita informações para uma ação 2. Nos scripts anteriores, a solicitação é implementada pelo objeto Symfony [HttpFoundation\Request] e sua memória pelo atributo [HttpFoundation\Request::attributes];

Image

Existem bibliotecas de terceiros para atribuir uma memória de aplicação ao PHP. A nova versão do exercício prático mostra o uso de uma delas.