Skip to content

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:

Image

  • 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:

Image

  • 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.

Image

Esta arquitetura será implementada pelos seguintes scripts:

Image

18.2.1. Entidades trocadas entre camadas

Image

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]

Image

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]

Image

Image

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

Image

Image

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]:

Image

  • 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;

Image

  • em [6], continuamos;

Image

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

Image

  • 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:

Image

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]:

Image

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]:

Image

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

18.2.5.1. Testes para a camada [dao]

Image

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:

Image

18.2.5.2. Testes da camada [business]

Image

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:

Image

18.3. O cliente

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

Image

Esta arquitetura será implementada pelos seguintes scripts:

Image

18.3.1. Entidades trocadas entre camadas

Image

As entidades acima mencionadas foram todas descritas e já se encontram em uso:

18.3.2. A camada [dao]

Image

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]

Image

Image

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

Image

Image

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:

Image

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]:

1
2
3
4
    "user": {
        "login": "x",
        "passwd": "x"
},

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.

Image

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:

Image