18. Exercício prático – versão 8
Vamos retomar o exemplo de aplicativo – versão 5 (parágrafo com link) e transformá-lo em um aplicativo cliente/servidor.
18.1. Introduction
A arquitetura da versão 5 era a seguinte:

- a camada denominada [dao] (Data Access Objects) é responsável pelas interações com o banco de dados MySQL e o sistema de arquivos local;
- a camada denominada [métier] realiza o cálculo do imposto;
- o script principal é o coordenador: ele instancia as camadas [dao] e [métier] e, em seguida, interage com a camada [métier] para realizar as tarefas necessárias;
Vamos migrar essa arquitetura para a seguinte arquitetura cliente/servidor:

- Na camada [2], vamos utilizar a camada [dao] da versão 5, removendo os métodos de acesso ao sistema de arquivos local. Esses métodos serão migrados para a camada [dao] do cliente [6, 7];
- na [3], a camada [métier] permanecerá como a da versão 5, sem os métodos [executeBatchImpôts, saveResults], que serão migrados para a camada [dao] e [7] do cliente;
- em [4], o script do servidor deve ser escrito: ele deverá:
- criar as camadas [métier], [dao] e [3, 2];
- interagir com o script do cliente [5, 7];
- em [7], a camada [dao] do cliente deve ser criada:
- ela será um cliente HTTP do script do servidor [4, 5];
- ela herdará os métodos de acesso ao sistema de arquivos local da camada [dao] da versão 5;
- em [8], a camada [métier] do cliente respeitará a interface [InterfaceMetier] da versão 5. Sua implementação, no entanto, será diferente. Na versão 5, a camada [métier] realizava o cálculo do imposto. Aqui, é a camada [métier] do servidor que realiza esse cálculo. A camada [métier], portanto, chamará as camadas [dao] e [7] para se comunicar com o servidor e solicitar que ele calcule o imposto;
- em [9], o script de console deverá instanciar as camadas [dao, métier] do cliente e iniciar a execução deste;
18.2. O servidor
Estamos interessados na parte do servidor da aplicação.

Essa arquitetura será implementada pelos seguintes scripts:

18.2.1. As entidades trocadas entre as camadas

As entidades trocadas entre as camadas são as da versão 5 descritas no parágrafo “link”.
18.2.2. A camada [dao]

A camada [dao] implementa a seguinte interface [InterfaceServerDao]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceServerDao {
// Leitura dos dados da administração fiscal
public function getTaxAdminData(): TaxAdminData;
}
- linha 9: o método [getTaxAdminData] busca os dados da administração fiscal em um banco de dados;
A interface [InterfaceServerDao] é implementada pela seguinte classe [ServerDao]:
<?php
// espaço de nomes
namespace Application;
// definição de uma classe ImpotsWithDataInDatabase
class ServerDao implements InterfaceServerDao {
// o objeto do tipo TaxAdminData que contém os dados das faixas de imposto
private $taxAdminData;
// o objeto do tipo [Database] que contém as características do BD
private $database;
// fabricante
public function __construct(string $databaseFilename) {
// a configuração JSON do banco de dados é armazenada
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// prepara-se o atributo
$this->taxAdminData = new TaxAdminData();
try {
// abrimos a conexão com o banco de dados
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// deseja-se que, a cada erro de SGBD, seja lançada uma exceção
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// inicia-se uma transação
$connexion->beginTransaction();
// preenche-se a tabela de faixas de imposto
$this->getTranches($connexion);
// preenche-se a tabela de constantes
$this->getConstantes($connexion);
// a transação é encerrada com sucesso
$connexion->commit();
} catch (\PDOException $ex) {
// há alguma transação em andamento?
if (isset($connexion) && $connexion->inTransaction()) {
// encerra-se a transação com falha
$connexion->rollBack();
}
// a exceção é repassada ao código chamador
throw new ExceptionImpots($ex->getMessage());
} finally {
// a conexão é encerrada
$connexion = NULL;
}
}
// Leitura dos dados do banco de dados
private function getTranches($connexion): void {
…
}
// leitura da tabela de constantes
private function getConstantes($connexion): void {
…
}
// retorna os dados necessários para o cálculo do imposto
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Esse código foi apresentado no parágrafo “link”.
18.2.3. A camada [métier]


A camada [métier] implementa a seguinte interface [InterfaceServerMetier]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceServerMetier {
// cálculo dos impostos de um contribuinte
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
A interface [InterfaceServerMetier] é implementada pela seguinte classe [ServerMetier]:
<?php
// espaço de nomes
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// camada DAO
private $dao;
// dados da administração tributária
private $taxAdminData;
//---------------------------------------------
// setter da camada [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// armazena-se uma referência na camada [dao]
$this->dao = $dao;
// recuperam-se os dados que permitem o cálculo do imposto
// o método [getTaxAdminData] pode lançar uma exceção ExceptionImpots
// em seguida, permite que a exceção seja repassada ao código chamador
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// cálculo do imposto
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// resultado
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
// --------------------------------------------------------------------------
private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
…
// resultado
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=salárioAnual-abatimento
// a dedução tem um valor mínimo e um valor máximo
private function getRevenuImposable(float $salaire): float {
…
// resultado
return floor($revenuImposable);
}
// calcula uma eventual redução
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// resultado
return ceil($décôte);
}
// calcula uma eventual redução
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// resultado
return ceil($réduction);
}
}
Esse código já foi apresentado e comentado na versão 1, no parágrafo [link]. Sua versão orientada a objetos com um banco de dados foi apresentada no parágrafo [link].
18.2.4. O script do servidor


O script do servidor implementa a camada [web] [4]. O script [impots-server] é configurado pelo seguinte arquivo jSON [config-server.json]:
{
"rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08",
"databaseFilename": "Data/database.json",
"taxAdminDataFileName": "Data/taxadmindata.json",
"relativeDependencies": [
"/Entities/BaseEntity.php",
"/Entities/ExceptionImpots.php",
"/Entities/TaxAdminData.php",
"/Entities/Database.php",
"/Dao/InterfaceServerDao.php",
"/Dao/ServerDao.php",
"/Métier/InterfaceServerMetier.php",
"/Métier/ServerMetier.php"
],
"absoluteDependencies": ["C:/myprograms/laragon-lite/www/vendor/autoload.php"],
"users": [
{
"login": "admin",
"passwd": "admin"
}
]
}
- linha 1: a pasta raiz a partir da qual os caminhos dos arquivos serão medidos;
- linha 2: o arquivo de configuração do banco de dados jSON;
- linha 3: o arquivo jSON com os dados da administração fiscal;
- linhas 5-14: os arquivos do aplicativo;
- linha 15: a dependência necessária das bibliotecas de terceiros, neste caso, o Symfony;
- linhas 16-20: a tabela de usuários autorizados a utilizar o aplicativo;
Os arquivos jSON e [database.json, taxadmindata.json] são os da versão 5 descrita no parágrafo “link”.
O script [impots-server] implementa a camada [web] da seguinte maneira:
<?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-se a configuração
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// inclui-se as dependências necessárias para o 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";
}
// definição das constantes
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// dependências do Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// preparação da resposta JSON do servidor
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// recuperação da solicitação atual
$request = Request::createFromGlobals();
// autenticação
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// o usuário existe?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// definindo o código de status da resposta
if (!$trouvé) {
// não encontrado – código 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// mensagem de erro
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// fim
exit;
}
// temos um usuário válido — verificamos os parâmetros recebidos
$erreurs = [];
// devemos ter três parâmetros GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// erro?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// recuperando o estado civil
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// erro?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// recupera-se o número de filhos
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// o número de filhos deve ser um número inteiro >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// erro?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// recuperando o salário anual
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// o salário deve ser um número inteiro >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// erro?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// outros parâmetros na consulta?
foreach (\array_keys($request->query->all()) as $key) {
// parâmetro válido?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// erros?
if ($erreurs) {
// envia-se um código de erro 400 ao cliente
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// temos tudo o que é necessário para trabalhar
// criação da arquitetura do servidor
$msgErreur = "";
try {
// criação da camada [dao]
$dao = new ServerDao($config["databaseFilename"]);
// criação da camada [métier]
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// observa-se o erro
$msgErreur = utf8_encode($ex->getMessage());
}
// erro?
if ($msgErreur) {
// envia-se um código de erro 500 ao cliente
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// cálculo do imposto
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// retornamos a resposta
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Comentários
- linha 16: o arquivo de configuração é processado;
- linhas 18-26: carregam-se todas as dependências;
- linha 29: o nome do arquivo [database.json];
- linhas 32-33: declara-se as classes das bibliotecas de terceiros que serão utilizadas;
- linhas 36-38: prepara-se uma resposta jSON;
- linhas 40-52: verifica-se se o usuário que está fazendo a solicitação faz parte dos usuários autorizados;
- linhas 54-63: caso contrário, envia-se o código HTTP 401, que indica uma recusa de acesso. Ao receber esse código e o cabeçalho HTTP [WWW-Authenticate => Basic realm=], a maioria dos navegadores exibe uma janela de autenticação solicitando que o usuário se autentique;
- linha 59: a resposta jSON do servidor explica a causa do erro. Todas as respostas do servidor serão a sequência jSON de uma tabela [‘réponse’=>’qq chose’];
- linhas 64-117: verifica-se a validade da solicitação:
- uma solicitação GET com exatamente três parâmetros;
- um parâmetro [marié] cujo valor deve ser ‘sim’ ou ‘não’;
- um parâmetro [enfants] cujo valor deve ser um número inteiro >=0;
- um parâmetro [salaire] cujo valor deve ser um número inteiro >=0;
- linha 65: sempre que um erro for detectado, uma mensagem de erro é adicionada à tabela [$erreurs];
- linhas 120-126: se houver erro, envia-se o código HTTP [400 Bad Request] ao cliente (linha 122);
- linha 123: a resposta jSON do servidor explica a causa do erro;
- a partir da linha 132, tudo foi verificado. É possível instanciar as camadas [dao, métier]. Essa instanciação tem um custo e só deve ser feita se tivermos certeza de que a solicitação é válida;
- linhas 130-138: cria-se a arquitetura do servidor. A construção da camada [dao] pode gerar uma exceção do tipo [ExceptionImpots]. Se essa exceção ocorrer, registra-se o erro;
- linhas 135-138: se tiver ocorrido uma exceção, envia-se o código HTTP 500 ao cliente. Esse código significa que o servidor apresentou uma falha;
- linha 143: a resposta explica a causa do erro;
- linha 148 : o cálculo do imposto é delegado à camada [métier];
- linhas 150-151: envio da resposta;
Vamos testar esse script com um navegador. Vamos solicitar a página segura URL a partir da [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- em [1], a resposta segura URL solicitada;
- em [2], os três parâmetros [marié, enfants, salaire];
- em [3], o servidor Apache do Laragon enviou um certificado SSL autoassinado. O navegador detectou isso e exibe um aviso de segurança: ele considera que o site do servidor não é confiável;
- em [4], continuamos;

- em [6], continuamos;

- em [7], o navegador exibe uma janela para que o usuário possa se autenticar;
- em [9,10], digitamos [admin] e [admin];

- em [13], a resposta jSON do servidor;
Vamos fazer alguns testes de erro:
Solicitamos o URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=x&enfants=x&salaire=x&w=x]
Obtemos o seguinte resultado:

Interrompemos o SGBD MySQL e solicitamos o URL [https://localhost/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=3&salaire=60000]:

18.2.5. Testes [Codeception]
Sempre que criarmos uma nova versão do servidor, testaremos as camadas [métier] e [dao], conforme tem sido feito desde a versão 04 (ver parágrafos link e link).
Primeiramente, associamos o projeto [scripts-web] aos testes [Codeception]. Para isso, siga o mesmo procedimento utilizado para o projeto [scripts-console] no parágrafo link. Obtemos um projeto [scripts-web] com uma pasta [Test Files]:

Vamos criar um teste para a camada [dao] e outro para a camada [métier].
18.2.5.1. Testes da camada [dao]

O teste [ServerDaoTest] será 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 das constantes
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// caminho do arquivo de configuração
define("CONFIG_FILENAME", ROOT . "/Data/config-server.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["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 ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// testes
public function testTaxAdminData() {
…
}
}
Comentários
- linhas 9-24: cria-se o mesmo ambiente de trabalho que o do servidor [impots-server.php]. Isso é feito nas linhas 9-12 com a definição das duas constantes das quais o ambiente depende;
- linhas 32-40: cria-se uma instância da camada [dao] a ser testada, da mesma forma que foi feito no script do servidor [impots-server.php];
- a partir de agora, estamos nas mesmas condições que o script do servidor [impots-server.php]: podemos iniciar os testes;
- linhas 43-45: o método [testTaxAdminData] é aquele descrito no parágrafo “link”;
Os resultados do teste são os seguintes:

18.2.5.2. Testes da camada [métier]

O teste [ServerMetierTest] será 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 das constantes
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// 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";
}
// classe de teste
class ServerMetierTest extends \Codeception\Test\Unit {
// camada de negócios
private $métier;
public function __construct() {
parent::__construct();
// recuperamos a configuração
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// criação da camada [dao]
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// criação da camada [métier]
$this->métier = new ServerMetier($dao);
}
// testes
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Comentários
- linhas 9-24: cria-se o mesmo ambiente de trabalho que o do servidor [impots-server.php]. Isso é feito nas linhas 9-12 com a definição das duas constantes das quais o ambiente depende;
- linhas 30-38: cria-se uma instância da camada [métier] a ser testada, da mesma forma que foi feito no script do servidor [impots-server.php];
- a partir de agora, estamos nas mesmas condições que o script do servidor [impots-server.php]: podemos iniciar os testes;
- linhas 40-53: os métodos [test1, test2…, test11] são os descritos no parágrafo “link”;
Os resultados do teste são os seguintes:

18.3. O cliente
Estamos interessados na parte do cliente do aplicativo.

Essa arquitetura será implementada pelos seguintes scripts:

18.3.1. As entidades trocadas entre camadas

Todas as entidades acima foram descritas e já utilizadas:
- [BaseEntity] no parágrafo “link”;
- [ExceptionImpots] no parágrafo “link”;
- [TaxPayerData] no parágrafo “link”;
18.3.2. A camada [dao]

A camada [dao] implementa a seguinte interface [InterfaceClientDao]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceClientDao {
// leitura dos dados do contribuinte
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// cálculo dos impostos de um contribuinte
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// registro dos resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- linha 9: a função [getTaxPayersData] carrega na memória os dados dos contribuintes do arquivo [$taxPayersFilename]. Caso haja erros, eles são registrados no arquivo [$errorsFilename];
- linha 12: a função [calculerImpots] calcula o imposto de um contribuinte;
- linha 15: a função [saveResults] salva no arquivo [$resultsFilename] os dados da tabela [$taxPayersData], que representam os resultados de vários cálculos de impostos;
A interface [InterfaceClientDao] é implementada pela seguinte classe [ClientDao]:
<?php
namespace Application;
// dependências
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// utilização de um Trait
use TraitDao;
// atributos
private $urlServer;
private $user;
// 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 {
// 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
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// recuperação da resposta
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// registros
// print "$json=json\n";
// recuperamos o status da resposta
$statusCode = $response->getStatusCode();
// erro?
if ($statusCode !== 200) {
// ocorreu um erro — lançamos uma exceção
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// retornamos a resposta
return $réponse;
}
}
Comentários
- linha 10: insere-se [TraitDao] (ver parágrafo “link”), que implementa os métodos [getTaxPayersData] e [saveResults]. Resta, portanto, apenas o método [calculerImpots] a ser implementado. Este está implementado nas linhas 22 a 49;
- linhas 16 a 19: o construtor da classe [ClientDao] recebe dois parâmetros:
- o URL [$urlServer] do servidor de cálculo de impostos;
- a matriz [$user] com as chaves ‘login’ e ‘passwd’, que define o usuário que está fazendo a solicitação;
- linha 22: o método [calculerImpots] recebe os três parâmetros a serem enviados ao servidor de cálculo de impostos;
- linhas 24-27: cria-se um cliente HTTP com:
- linha 25: os dados de identificação do usuário que está fazendo a solicitação;
- linha 26: a opção que faz com que o cliente HTTP não verifique a validade do certificado SSL enviado pelo servidor;
- linhas 29-34: o servidor é consultado com os três parâmetros que ele espera;
- linha 36: é recuperada a resposta jSON do servidor. Se o parâmetro [false] não for passado ao método [Response::getContent], então, caso o status da resposta do servidor esteja no intervalo [3xx-5xx] (caso de erro), o objeto [Response] lança uma exceção assim que se tenta obter o conteúdo da resposta [Response::getContent] ou seus cabeçalhos HTTP e [Response::getHeaders]. Aqui, independentemente do status HTTP da resposta, queremos poder acessar o conteúdo dela, nem que seja apenas para registrá-lo no log (linha 40);
- linhas 37-38: a resposta do servidor é a string jSON de uma matriz [‘réponse’=>qqChose]. Recuperamos o [qqChose];
- linha 40: registramos a resposta jSON no modo de desenvolvimento;
- linha 42: recuperamos o código de status da resposta;
- linhas 44-49: se o código de status HTTP não for 200, isso significa que nosso servidor encontrou um problema. Em seguida, lança-se uma exceção do tipo [ExceptionImpots] com a mensagem correspondente à resposta jSON do servidor, acrescida do código HTTP da resposta;
- linha 51: retornamos o resultado, que é um array associativo com as chaves [impôt, surcôte, décôte, réduction, taux];
18.3.3. A camada [métier]


A camada [métier] [8] implementa a seguinte interface [InterfaceClientMetier]:
<?php
// espaço de nomes
namespace Application;
interface InterfaceClientMetier {
// cálculo dos impostos de um contribuinte
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- linha 9: a função [calculerImpots] calcula o imposto;
- linha 12: a função [executeBatchImpots] calcula o imposto dos contribuintes cujos dados constam no arquivo [$taxPayersFileName], grava os resultados obtidos no arquivo [$resultsFileName] e os erros encontrados no arquivo [$errorsFileName];
A interface [InterfaceClientMetier] é implementada pela seguinte classe [ClientMetier]:
<?php
// espaço de nomes
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// atributo
private $clientDao;
// construtor
public function __construct(InterfaceClientDao $clientDao) {
// a referência é armazenada na camada [dao]
$this->clientDao = $clientDao;
}
// cálculo do imposto
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// cálculo de impostos em modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// permite que as exceções provenientes da camada [dao] sejam encaminhadas
// recuperação dos dados dos contribuintes
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabela de resultados
$results = [];
// analisamos os dados
foreach ($taxPayersData as $taxPayerData) {
// calcula-se o imposto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// preenche-se [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// insere-se o resultado na tabela de resultados
$results [] = $taxPayerData;
}
// gravação dos resultados
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Comentários
- linhas 11-14: o construtor da classe [ClientMetier] recebe como parâmetro uma referência à camada [dao];
- linhas 17-19: o cálculo do imposto é delegado à camada [dao];
- linhas 20-38: a função [executeBatchImpots] foi descrita no parágrafo “link”;
18.3.4. O script principal


O script cliente [MainImpotsClient.php] implementa as camadas [console] e [9]. Ele é configurado pelo seguinte arquivo jSON e [conf-client.json]:
{
"rootDirectory": "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08",
"taxPayersDataFileName": "Data/taxpayersdata.json",
"resultsFileName": "Data/results.json",
"errorsFileName": "Data/errors.json",
"dependencies": [
"Entities/BaseEntity.php",
"Entities/TaxPayerData.php",
"Entities/ExceptionImpots.php",
"Utilities/Utilitaires.php",
"Dao/InterfaceClientDao.php",
"Dao/TraitDao.php",
"Dao/ClientDao.php",
"Métier/InterfaceClientMetier.php",
"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-08/impots-server.php"
}
- linha 1: a pasta raiz do cliente;
- linha 2: o arquivo jSON com os dados dos contribuintes;
- linha 3: o arquivo jSON dos resultados;
- linha 4: o arquivo jSON de erros;
- linhas 6-19: as diferentes dependências do projeto do cliente;
- linhas 20-23: o usuário que realiza as consultas ao servidor de cálculo de impostos;
- linha 24: o arquivo URL seguro do servidor de cálculo de impostos;
O código do script [MainImpotsClient.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;
// gestão de erros por PHP
//ini_set("display_errors", "0");
//
// caminho do arquivo de configuração
define("CONFIG_FILENAME", "../Data/config-client.json");
// recupera-se 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";
}
// definição de constantes
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// dependências do Symfony
use Symfony\Component\HttpClient\HttpClient;
// criação da camada [dao]
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// criação da camada [métier]
$clientMetier = new ClientMetier($clientDao);
// cálculo de impostos em modo batch
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// exibição do erro
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// fim
print "Terminé\n";
exit;
Comentários
- linha 13: caminho do arquivo de configuração;
- linha 16: processamento do arquivo de configuração;
- linhas 18-26: carregamento das dependências;
- linha 37: criação da camada [dao]. Passamos ao construtor da camada as duas informações que ele espera:
- o URL do servidor de cálculo de impostos;
- os dados de identificação do usuário que fará as consultas;
- linha 39: criação da camada [métier]. Passamos ao construtor da camada uma referência à camada [dao] que acaba de ser criada;
- linha 43: solicita-se à camada [métier] que:
- calcular os impostos de todos os contribuintes do arquivo $config["taxPayerDataFileName"];
- gravar os resultados no arquivo $config["resultsFileName"];
- gravar os erros no arquivo $config["errorsFileName"];
- a linha 43 pode gerar exceções;
- linha 46: exibição da mensagem de erro da exceção;
A execução do cliente produz os mesmos resultados que as versões anteriores. Verifique os seguintes arquivos:
- [Data/taxpayersdata.json]: dados dos contribuintes para os quais se calcula o valor do imposto;
- [Data/results.json]: resultados para os diferentes contribuintes do arquivo [Data/taxpayersdata.json];
- [Data/errors.json]: os erros que possam ter ocorrido durante a processamento do arquivo [Data/taxpayersdata.json];
Vamos examinar os possíveis casos de erro. Primeiro, vamos parar o servidor Laragon. Os resultados no console do cliente são então os seguintes:
Couldn't connect to server for"https://localhost/php7/scripts-web/impots/version-08/impots-server.php?mari%C3%A9=oui&enfants=2&salaire=55555".
Terminé
Agora, vamos iniciar apenas o servidor Apache, sem o SGBD MySQL:

Os resultados no console do cliente são então os seguintes:
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é
Agora, vamos iniciar o MySQL e, em seguida, alterar no [config-client] o usuário que está fazendo login:
Os resultados no console do cliente são então os seguintes:
L'erreur suivante s'est produite : {"statut HTTP":401,"erreur":"Echec de l'authentification [x, x]"}
Terminé
18.3.5. Testes [Codeception]
Assim como fizemos nas versões anteriores, vamos escrever os testes [Codeception] para a versão 08.

18.3.5.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 das constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
// 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";
}
//
// classe de teste
class ClientMetierTest extends \Codeception\Test\Unit {
// camada de negócios
private $métier;
public function __construct() {
parent::__construct();
// recuperamos a 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 test11() {
…
}
}
Comentários
- linhas 10-26: definição do ambiente de teste. Utilizamos o mesmo ambiente usado pelo script principal [MainImpotsClient], descrito no parágrafo com o link;
- linhas 33-41: construção das camadas [dao] e [métier];
- linha 40: o atributo [$this→métier] faz referência à camada [métier];
- linhas 44-51: os métodos [test1, test2…, test11] são os descritos no parágrafo “link”;
Os resultados do teste são os seguintes:
