18. Exercício prático – Versão 8
Vamos revisitar a aplicação de exemplo – versão 5 (parágrafo do link) e transformá-la numa aplicação cliente/servidor.
18.1. Introdução
A arquitetura da versão 5 era a seguinte:

- a camada denominada [dao] (Data Access Objects) lida com as interações com a base de dados MySQL e o sistema de ficheiros local;
- a camada denominada [business] realiza o cálculo dos impostos;
- o script principal atua como orquestrador: instancia as camadas [DAO] e [business logic] e, em seguida, comunica com a camada [business logic] para executar as tarefas necessárias;
Iremos migrar esta arquitetura para a seguinte arquitetura cliente/servidor:

- Em [2], iremos reutilizar a camada [DAO] da versão 5, removendo os métodos de acesso ao sistema de ficheiros local. Estes métodos serão migrados para a camada [DAO] do cliente [6, 7];
- Em [3], a camada [business] permanecerá a mesma da versão 5, sem os métodos [executeBatchImpôts, saveResults], que serão migrados para a camada [DAO] do cliente [7];
- Em [4], o script do servidor deve ser escrito: ele precisará:
- criar as camadas [business] e [DAO] [3, 2];
- comunicar com o script do cliente [5, 7];
- Em [7], a camada [dao] do cliente deve ser escrita:
- será um cliente HTTP do script do servidor [4, 5];
- reutilizará os métodos para aceder ao sistema de ficheiros local a partir da camada [dao] da versão 5;
- em [8], a camada [business] do cliente estará em conformidade com a interface [BusinessInterface] da versão 5. A sua implementação será, no entanto, diferente. Na versão 5, a camada [business] realizava o cálculo do imposto. Aqui, é a camada [business] do servidor que realiza este cálculo. A camada [business] irá, portanto, recorrer à camada [DAO] [7] para comunicar com o servidor e solicitar que este calcule o imposto;
- em [9], o script da consola terá de instanciar as camadas [DAO, business] do cliente e iniciar a sua execução;
18.2. O servidor
Estamos a concentrar-nos no lado do servidor da aplicação.

Esta arquitetura será implementada pelos seguintes scripts:

18.2.1. Entidades trocadas entre camadas

As entidades trocadas entre camadas são as da versão 5 descritas na secção indicada no link.
18.2.2. A camada [dao]

A camada [dao] implementa a seguinte interface [InterfaceServerDao]:
<?php
// namespace
namespace Application;
interface InterfaceServerDao {
// reading tax administration data
public function getTaxAdminData(): TaxAdminData;
}
- Linha 9: O método [getTaxAdminData] recupera dados da administração fiscal de uma base de dados;
A interface [InterfaceServerDao] é implementada pela seguinte classe [ServerDao]:
<?php
// namespace
namespace Application;
// definition of a ImpotsWithDataInDatabase class
class ServerDao implements InterfaceServerDao {
// the TaxAdminData object containing tax bracket data
private $taxAdminData;
// the [Database] type object containing the characteristics of the BD
private $database;
// manufacturer
public function __construct(string $databaseFilename) {
// store the JSON configuration of the bd
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// we prepare the attribute
$this->taxAdminData = new TaxAdminData();
try {
// open the database connection
$connexion = new \PDO($this->database->getDsn(), $this->database->getId(), $this->database->getPwd());
// we want every SGBD error to trigger an exception
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// start a transaction
$connexion->beginTransaction();
// fill in the tax bracket table
$this->getTranches($connexion);
// fill in the constants table
$this->getConstantes($connexion);
// the transaction is completed successfully
$connexion->commit();
} catch (\PDOException $ex) {
// is there a transaction in progress?
if (isset($connexion) && $connexion->inTransaction()) {
// transaction ends in failure
$connexion->rollBack();
}
// trace the exception back to the calling code
throw new ExceptionImpots($ex->getMessage());
} finally {
// close the connection
$connexion = NULL;
}
}
// reading data from the database
private function getTranches($connexion): void {
…
}
// reading the constants table
private function getConstantes($connexion): void {
…
}
// returns data for tax calculation
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Este código foi apresentado na secção com o link.
18.2.3. A camada [de negócios]


A camada [business] implementa a seguinte interface [InterfaceServerMetier]:
<?php
// namespace
namespace Application;
interface InterfaceServerMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
}
A interface [InterfaceServerMetier] é implementada pela seguinte classe [ServerMetier]:
<?php
// namespace
namespace Application;
class ServerMetier implements InterfaceServerMetier {
// dao layer
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceServerDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceServerDao $dao) {
// a reference is stored on the [dao] layer
$this->dao = $dao;
// recover data for tax calculation
// method [getTaxAdminData] may throw a ExceptionImpots exception
// we then let it go back to the calling code
$this->taxAdminData = $this->dao->getTaxAdminData();
}
// tAX CALCULATION
// --------------------------------------------------------------------------
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
…
// result
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 {
…
// result
return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
}
// revenuImposable=annualwage-discount
// the allowance has a minimum and a maximum
private function getRevenuImposable(float $salaire): float {
…
// result
return floor($revenuImposable);
}
// calculates any discount
private function getDecôte(string $marié, float $salaire, float $impots): float {
…
// result
return ceil($décôte);
}
// calculates any reduction
private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
..
// result
return ceil($réduction);
}
}
Este código já foi abordado na Versão 1 na secção indicada. A sua versão orientada para objetos com uma base de dados foi apresentada na secção indicada.
18.2.4. O script do servidor


O script do servidor implementa a camada [web] [4]. O script [impots-server] é configurado pelo seguinte ficheiro 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: o diretório raiz a partir do qual os caminhos dos ficheiros serão medidos;
- linha 2: o ficheiro de configuração JSON para a base de dados MySQL;
- linha 3: o ficheiro JSON contendo dados de administração fiscal;
- linhas 5–14: os ficheiros da aplicação;
- linha 15: a dependência de bibliotecas de terceiros, neste caso Symfony;
- linhas 16–20: a matriz de utilizadores autorizados a utilizar a aplicação;
Os ficheiros JSON [database.json, taxadmindata.json] são os da versão 5, conforme descrito na secção indicada no link.
O script [impots-server] implementa a camada [web] da seguinte forma:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definition of constants
define("DATABASE_CONFIG_FILENAME", $config["databaseFilename"]);
//
// symfony dependencies
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
// prepare JSON server response
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$users = $config["users"];
$i = 0;
$trouvé = FALSE;
while (!$trouvé && $i < count($users)) {
$trouvé = ($requestUser === $users[$i]["login"] && $users[$i]["passwd"] === $requestPassword);
$i++;
}
// set the response status code
if (!$trouvé) {
// not found - code 401
$response->setStatusCode(Response::HTTP_UNAUTHORIZED);
$response->headers->add(["WWW-Authenticate" => "Basic realm=" . utf8_decode("\"Serveur de calcul d'impôts\"")]);
// error msg
$response->setContent(\json_encode(["réponse" => ["erreur" => "Echec de l'authentification [$requestUser, $requestPassword]"]], JSON_UNESCAPED_UNICODE));
$response->send();
// end
exit;
}
// we have a valid user - we check the parameters received
$erreurs = [];
// you need three parameters GET
$method = strtolower($request->getMethod());
$erreur = $method !== "get" || $request->query->count() != 3;
// mistake?
if ($erreur) {
$erreurs[] = "Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]";
}
// marital status is restored
if (!$request->query->has("marié")) {
$erreurs[] = "paramètre marié manquant";
} else {
$marié = trim(strtolower($request->query->get("marié")));
$erreur = $marié !== "oui" && $marié !== "non";
// mistake?
if ($erreur) {
$erreurs[] = "paramètre marié [$marié] invalide";
}
}
// the number of children
if (!$request->query->has("enfants")) {
$erreurs[] = "paramètre enfants manquant";
} else {
$enfants = trim($request->query->get("enfants"));
// number of children must be an integer >=0
$erreur = !preg_match("/^\d+$/", $enfants);
// mistake?
if ($erreur) {
$erreurs[] = "paramètre enfants [$enfants] invalide";
}
}
// we recover the annual salary
if (!$request->query->has("salaire")) {
$erreurs[] = "paramètre salaire manquant";
} else {
// salary must be an integer >=0
$salaire = trim($request->query->get("salaire"));
$erreur = !preg_match("/^\d+$/", $salaire);
// mistake?
if ($erreur) {
$erreurs[] = "paramètre salaire [$salaire] invalide";
}
}
// other parameters in the query?
foreach (\array_keys($request->query->all()) as $key) {
// valid parameter?
if (!\in_array($key, ["marié", "enfants", "salaire"])) {
$erreurs[] = "paramètre [$key] invalide";}
}
// mistakes?
if ($erreurs) {
// an error code 400 is sent to the customer
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
$response->setContent(json_encode(["réponse" => ["erreurs" => $erreurs]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// we have everything you need to work
// server architecture creation
$msgErreur = "";
try {
// creation of the [dao] layer
$dao = new ServerDao($config["databaseFilename"]);
// creation of the [business] layer
$métier = new ServerMetier($dao);
} catch (ExceptionImpots $ex) {
// we note the error
$msgErreur = utf8_encode($ex->getMessage());
}
// mistake?
if ($msgErreur) {
// an error code 500 is sent to the customer
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
$response->setContent(\json_encode(["réponse" => ["erreur" => $msgErreur]], JSON_UNESCAPED_UNICODE));
$response->send();
exit;
}
// tAX CALCULATION
$result = $métier->calculerImpot($marié, (int) $enfants, (int) $salaire);
// we return the answer
$response->setContent(json_encode(["réponse" => $result], JSON_UNESCAPED_UNICODE));
$response->send();
Comentários
- linha 16: carregamos o ficheiro de configuração;
- linhas 18–26: carregamos todas as dependências;
- linha 29: o nome do ficheiro [database.json];
- linhas 32–33: declaramos as classes das bibliotecas de terceiros que serão utilizadas;
- linhas 36–38: preparamos uma resposta JSON;
- linhas 40–52: verificamos se o utilizador que faz o pedido é, de facto, um utilizador autorizado;
- linhas 54–63: caso contrário, enviamos um código HTTP 401 indicando acesso negado. Ao receber este código e o cabeçalho HTTP [WWW-Authenticate => Basic realm=], a maioria dos navegadores exibe uma janela de autenticação solicitando que o utilizador inicie sessão;
- linha 59: a resposta JSON do servidor explica a causa do erro. Todas as respostas do servidor serão uma cadeia JSON na forma de uma matriz [‘response’=>’something’];
- Linhas 64–117: Verificamos a validade do pedido:
- uma solicitação GET com exatamente três parâmetros;
- um parâmetro [married] cujo valor deve ser «yes» ou «no»;
- um parâmetro [children] cujo valor deve ser um número inteiro >= 0;
- um parâmetro [salary] cujo valor deve ser um número inteiro >= 0;
- linha 65: sempre que é detetado um erro, é adicionada uma mensagem de erro à matriz [$errors];
- linhas 120–126: se ocorrer um erro, o código HTTP [400 Bad Request] é enviado ao cliente (linha 122);
- linha 123: a resposta JSON do servidor explica a causa do erro;
- A partir da linha 132, tudo foi verificado. Podemos instanciar as camadas [dao, business]. Esta instanciação tem um custo e só deve ser feita se tivermos a certeza de que temos um pedido válido;
- linhas 130–138: a arquitetura do servidor é criada. A construção da camada [DAO] pode lançar uma exceção [ExceptionImpots]. Se esta exceção ocorrer, o erro é registado;
- linhas 135–138: se ocorreu uma exceção, enviamos o código de estado HTTP 500 ao cliente. Este código indica que o servidor falhou;
- linha 143: a resposta explica a causa do erro;
- linha 148 : o cálculo do imposto é delegado à camada [business];
- linhas 150–151: enviar a resposta;
Vamos testar este script com um navegador. Vamos solicitar o URL seguro [https://localhost:443/php7/scripts-web/impots/version-08/impots-server.php?marié=oui&enfants=5&salaire=100000]:

- em [1], a URL segura solicitada;
- em [2], os três parâmetros [casado, filhos, salário];
- em [3], o servidor Apache do Laragon enviou um certificado SSL autoassinado. O navegador detetou isto e apresenta um aviso de segurança: considera que o site do servidor não é fiável;
- em [4], continuamos;

- em [6], continuamos;

- Em [7], o navegador exibe uma janela para o utilizador iniciar sessão;
- em [9,10], digite [admin] e [admin];

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

Desligamos o SGBD MySQL e solicitamos a 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 compilarmos uma nova versão do servidor, testaremos as camadas [business] e [DAO], tal como tem sido feito desde a versão 04 (ver parágrafos link e link).
Primeiro, associamos o projeto [scripts-web] aos testes [Codeception]. Para tal, siga o mesmo procedimento utilizado para o projeto [scripts-console] descrito no parágrafo com o link. Ficamos com um projeto [scripts-web] que contém uma pasta [Test Files]:

Vamos criar um teste para a camada [dao] e outro para a camada [business].
18.2.5.1. Testes para a camada [dao]

O [ServerDaoTest] será o seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test -----------------------------------------------------
class ServerDaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
// parent
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Comentários
- linhas 9–24: Configuramos o mesmo ambiente de trabalho que o do servidor [impots-server.php]. Isto é feito nas linhas 9–12, definindo as duas constantes das quais o ambiente depende;
- linhas 32–40: criamos uma instância da camada [dao] a ser testada, tal como foi feito no script do servidor [impots-server.php];
- A partir deste ponto, 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 na secção indicada;
Os resultados do teste são os seguintes:

18.2.5.2. Testes da camada [business]

O teste [ServerMetierTest] será o seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-server.json");
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["relativeDependencies"] as $dependency) {
require "$rootDirectory$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// test class
class ServerMetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$dao = new ServerDao(ROOT . "/" . $config["databaseFilename"]);
// creation of the [business] layer
$this->métier = new ServerMetier($dao);
}
// tests
public function test1() {
…
}
public function test2() {
…
}
..
public function test11() {
…
}
}
Comentários
- Linhas 9–24: Configuramos o mesmo ambiente de trabalho que o do servidor [impots-server.php]. Isto é feito nas linhas 9–12, definindo as duas constantes das quais o ambiente depende;
- linhas 30–38: criamos uma instância da camada [business] a ser testada, tal como foi feito no script do servidor [impots-server.php];
- A partir deste ponto, 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 na secção indicada;
Os resultados dos testes são os seguintes:

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

Esta arquitetura será implementada pelos seguintes scripts:

18.3.1. Entidades trocadas entre camadas

As entidades acima mencionadas foram todas descritas e já se encontram em uso:
- [BaseEntity] na secção de ligações;
- [ExceptionImpots] na secção de ligações;
- [TaxPayerData] na secção de ligações;
18.3.2. A camada [dao]

A camada [dao] implementa a seguinte interface [InterfaceClientDao]:
<?php
// namespace
namespace Application;
interface InterfaceClientDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// recording results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
- linha 9: a função [getTaxPayersData] carrega os dados dos contribuintes do ficheiro [$taxPayersFilename] para a memória. Se ocorrerem erros, estes são registados no ficheiro [$errorsFilename];
- linha 12: a função [calculateTaxes] calcula o imposto de um contribuinte;
- Linha 15: a função [saveResults] guarda os dados da matriz [$taxPayersData] — que representa os resultados de vários cálculos de impostos — no ficheiro [$resultsFilename];
A interface [InterfaceClientDao] é implementada pela seguinte classe [ClientDao]:
<?php
namespace Application;
// dependencies
use \Symfony\Component\HttpClient\HttpClient;
class ClientDao implements InterfaceClientDao {
// using a Trait
use TraitDao;
// attributes
private $urlServer;
private $user;
// manufacturer
public function __construct(string $urlServer, array $user) {
$this->urlServer = $urlServer;
$this->user = $user;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// create a HTTP customer
$httpClient = HttpClient::create([
'auth_basic' => [$this->user["login"], $this->user["passwd"]],
"verify_peer" => false
]);
// make a request to the server
$response = $httpClient->request('GET', $this->urlServer,
["query" => [
"marié" => $marié,
"enfants" => $enfants,
"salaire" => $salaire
]]);
// the answer is retrieved
$json = $response->getContent(false);
$array = \json_decode($json, true);
$réponse = $array["réponse"];
// logs
// print "$json=json\n";
// retrieve response status
$statusCode = $response->getStatusCode();
// mistake?
if ($statusCode !== 200) {
// we have an error - we throw an exception
$réponse = ["statut HTTP" => $statusCode] + $réponse;
$message = \json_encode($réponse, JSON_UNESCAPED_UNICODE);
throw new ExceptionImpots($message);
}
// we return the answer
return $réponse;
}
}
Comentários
- linha 10: inserimos [TraitDao] (ver parágrafo em link), que implementa os métodos [getTaxPayersData] e [saveResults]. Isto deixa apenas o método [calculateTaxes] por implementar. Este é implementado nas linhas 22–49;
- linhas 16–19: o construtor da classe [ClientDao] recebe dois parâmetros:
- a URL [$urlServer] do servidor de cálculo de impostos;
- a matriz [$user] com as chaves «login» e «passwd» que define o utilizador que efetua o pedido;
- linha 22: o método [calculateTaxes] recebe os três parâmetros a enviar para o servidor de cálculo de impostos;
- linhas 24–27: é criado um cliente HTTP com:
- linha 25: as credenciais do utilizador que efetua o pedido;
- linha 26: a opção que impede o cliente HTTP de verificar a validade do certificado SSL enviado pelo servidor;
- linhas 29–34: o servidor é consultado com os três parâmetros que espera;
- linha 36: recuperamos a resposta JSON do servidor. Se não definirmos o parâmetro [false] no método [Response::getContent], então, se o estado da resposta do servidor estiver no intervalo [3xx-5xx] (caso de erro), o objeto [Response] lança uma exceção assim que tentarmos recuperar o conteúdo da resposta [Response::getContent] ou os seus cabeçalhos HTTP [Response::getHeaders]. Aqui, independentemente do estado HTTP da resposta, queremos poder aceder ao seu conteúdo, nem que seja apenas para o registar (linha 40);
- linhas 37-38: a resposta do servidor é uma cadeia JSON de um array [‘response’=>something]. Recuperamos o [something];
- linha 40: registamos a resposta JSON no modo de desenvolvimento;
- linha 42: recuperamos o código de estado da resposta;
- linhas 44-49: se o código de estado HTTP não for 200, então o nosso servidor encontrou um problema. Em seguida, lançamos uma exceção [ExceptionImpots] com uma mensagem composta pela resposta JSON do servidor, acompanhada do código de estado HTTP;
- linha 51: devolvemos o resultado, que é uma matriz associativa com as chaves [tax, surcharge, discount, reduction, rate];
18.3.3. A camada [de negócios]


A camada [de negócios] [8] implementa a seguinte [BusinessClientInterface]:
<?php
// namespace
namespace Application;
interface InterfaceClientMetier {
// calculating a taxpayer's taxes
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- linha 9: a função [calculateTaxes] calcula o imposto;
- linha 12: a função [executeBatchImpots] calcula o imposto para os contribuintes cujos dados se encontram no ficheiro [$taxPayersFileName], grava os resultados no ficheiro [$resultsFileName] e grava quaisquer erros encontrados no ficheiro [$errorsFileName];
A interface [BusinessClientInterface] é implementada pela seguinte classe [BusinessClient]:
<?php
// namespace
namespace Application;
class ClientMetier implements InterfaceClientMetier {
// attribute
private $clientDao;
// manufacturer
public function __construct(InterfaceClientDao $clientDao) {
// the reference is stored on the [dao] layer
$this->clientDao = $clientDao;
}
// tAX CALCULATION
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
return $this->clientDao->calculerImpot($marié, $enfants, $salaire);
}
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// we let the exceptions coming from the [dao] layer flow upwards
// retrieve taxpayer data
$taxPayersData = $this->clientDao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// results table
$results = [];
// we exploit them
foreach ($taxPayersData as $taxPayerData) {
// tax calculation
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// complete [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// put the result in the results table
$results [] = $taxPayerData;
}
// recording results
$this->clientDao->saveResults($resultsFileName, $results);
}
}
Comentários
- linhas 11–14: o construtor da classe [ClientMetier] recebe uma referência à camada [dao] como parâmetro;
- linhas 17–19: o cálculo do imposto é delegado à camada [dao];
- linhas 20–38: a função [executeBatchImpots] foi descrita na secção de links;
18.3.4. O script principal


O script do cliente [MainImpotsClient.php] implementa a camada [console] [9]. É configurado pelo seguinte ficheiro JSON [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: o diretório raiz do cliente;
- linha 2: o ficheiro JSON que contém os dados do contribuinte;
- linha 3: o ficheiro JSON contendo os resultados;
- linha 4: o ficheiro JSON que contém os erros;
- linhas 6–19: as várias dependências do projeto do cliente;
- linhas 20–23: o utilizador a enviar pedidos para o servidor de cálculo de impostos;
- linha 24: o URL seguro do servidor de cálculo de impostos;
O código do script [MainImpotsClient.php] é o seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// error handling by PHP
//ini_set("display_errors", "0");
//
// configuration file path
define("CONFIG_FILENAME", "../Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
// definition of constants
define("TAXPAYERSDATA_FILENAME", "$rootDirectory/{$config["taxPayersDataFileName"]}");
define("RESULTS_FILENAME", "$rootDirectory/{$config["resultsFileName"]}");
define("ERRORS_FILENAME", "$rootDirectory/{$config["errorsFileName"]}");
//
// symfony dependencies
use Symfony\Component\HttpClient\HttpClient;
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$clientMetier = new ClientMetier($clientDao);
// tax calculation in batch mode
try {
$clientMetier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (\RuntimeException $ex) {
// error is displayed
print "L'erreur suivante s'est produite : " . $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;
Comentários
- linha 13: caminho para o ficheiro de configuração;
- linha 16: processamento do ficheiro de configuração;
- linhas 18–26: carregamento de dependências;
- linha 37: criação da camada [dao]. Passamos as duas informações que o construtor da camada espera:
- o URL do servidor de cálculo de impostos;
- as credenciais do utilizador que irá efetuar os pedidos;
- linha 39: criação da camada [business]. Passamos uma referência à camada [dao] que acabou de ser criada para o construtor da camada;
- linha 43: solicitamos à camada [business] que:
- calcule os impostos para todos os contribuintes no ficheiro $config["taxPayerDataFileName"];
- escreva os resultados no ficheiro $config["resultsFileName"];
- escreva os erros no ficheiro $config["errorsFileName"];
- A linha 43 pode lançar exceções;
- Linha 46: Exibir a mensagem de erro da exceção;
A execução do cliente produz os mesmos resultados que as versões anteriores. Verifique os seguintes ficheiros:
- [Data/taxpayersdata.json]: dados dos contribuintes para os quais o montante do imposto é calculado;
- [Data/results.json]: resultados para os vários contribuintes no ficheiro [Data/taxpayersdata.json];
- [Data/errors.json]: erros que possam ter ocorrido durante o processamento do ficheiro [Data/taxpayersdata.json];
Vejamos os possíveis casos de erro. Primeiro, vamos parar o servidor Laragon. Os resultados na consola 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 e não o SGBD MySQL:

Os resultados na consola do cliente sã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, modificar o utilizador que se liga em [config-client]:
Os resultados na consola do cliente sã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]
Tal como fizemos nas versões anteriores, iremos escrever testes [Codeception] para a versão 08.

18.3.5.1. Testar a camada [business]
O teste [ClientMetierTest.php] é o seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// definition of constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-08");
// configuration file path
define("CONFIG_FILENAME", ROOT . "/Data/config-client.json");
// we retrieve the configuration
$config = \json_decode(file_get_contents(CONFIG_FILENAME), true);
// include the necessary script dependencies
$rootDirectory = $config["rootDirectory"];
foreach ($config["dependencies"] as $dependency) {
require "$rootDirectory/$dependency";
}
// absolute dependencies (third-party libraries)
foreach ($config["absoluteDependencies"] as $dependency) {
require "$dependency";
}
//
// test class
class ClientMetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILENAME), true);
// creation of the [dao] layer
$clientDao = new ClientDao($config["urlServer"], $config["user"]);
// creation of the [business] layer
$this->métier = new ClientMetier($clientDao);
}
// tests
public function test1() {
…
}
-------------
public function test11() {
…
}
}
Comentários
- linhas 10–26: definição do ambiente de teste. Utilizamos o mesmo que o script principal [MainImpotsClient] descrito na secção com o link;
- linhas 33–41: construção das camadas [dao] e [business];
- linha 40: o atributo [$this→business] faz referência à camada [business];
- linhas 44–51: os métodos [test1, test2…, test11] são os descritos na secção em link;
Os resultados do teste são os seguintes:
