Skip to content

11. Exercício IMPOTS com um serviço WEB e uma arquitetura de três camadas

Vamos retomar o exercício IMPOTS (ver parágrafos 4.2, 4.3 e 6) e transformá-lo numa aplicação cliente/servidor. O script do servidor será dividido em três elementos:

  • uma camada denominada [dao] (Data Access Objects), que se encarregará das interações com a base de dados MySQL
  • uma camada denominada [métier], que efetuará o cálculo do imposto
  • uma camada [web] que se encarregará das interações com os clientes web.

O script do cliente [1]:

  • transmite ao script do servidor as três informações ($marié, $enfants, $salaire) necessárias para o cálculo do imposto
  • exibe a resposta do servidor na consola

O script do servidor [2] é constituído pela camada [web] do servidor.

  • No início de uma nova sessão do cliente, irá colocar em tabelas os dados das bases de dados MySQL e [dbimpots]. Para tal, recorrerá à camada [dao]. As tabelas assim criadas serão colocadas na sessão do cliente para que possam ser utilizadas em consultas posteriores do cliente.
  • Durante uma solicitação do cliente, este transmitirá as três informações ($marié, $enfants, $salaire) à camada [métier], que calculará o imposto $impot.
  • O script do servidor devolverá o imposto $impôt calculado.

11.1. O script do cliente (clients_impots_05_web)

O script do cliente será um cliente do serviço web de cálculo do imposto. Enviará (POST) para o servidor parâmetros na forma:

params=$casado,$filhos,$salário, em que

  • $marié será a cadeia oui ou non,
  • $enfants será o número de filhos,
  • $salaire será o salário do contribuinte

Encontra os três parâmetros anteriores num ficheiro de texto [data.txt] na forma (casado, filhos, salário):

oui,2,200000
non,2,200000
oui,3,200000
non,3,200000
oui,5,50000
non,0,3000000

O script do cliente

  • irá ler o ficheiro de texto [data.txt] linha a linha
  • enviará a cadeia de caracteres params=$marié,$enfants,$salaire para o serviço web de cálculo do imposto
  • e irá recuperar a resposta do serviço. Esta resposta poderá assumir duas formas:
<erreur>message d'erreur</erreur>
<impot>montant de l'impôt</impot>
  • gravará a resposta do servidor num ficheiro de texto [resultats.txt] numa das duas formas seguintes:
marié:enfants:salaire:erreur
marié:enfants:salaire:impôt

O código do script do cliente é o seguinte:


<?php

// cliente de impostos
// gestão de erros
ini_set("display_errors", "off");

// ---------------------------------------------------------------------------------
// uma classe de funções utilitárias
class Utilitaires {

  function cutNewLinechar($ligne) {
    ...
  }

}

// main -----------------------------------------------------
// definição de constantes
$DATA = "data.txt";
$RESULTATS = "resultats.txt";
// dados do servidor
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/impots_05_web.php";

// parâmetros dos contribuintes (estado civil, número de filhos, salário anual)
// foram inseridos no ficheiro de texto $DATA, à razão de uma linha por contribuinte
// os resultados (estado civil, número de filhos, salário anual, imposto a pagar) 
// ou (estado civil, número de filhos, salário anual, mensagem de erro) são colocados no
// no ficheiro de texto $RESULTATS, à razão de um resultado por linha

// classe Utilitários
$u = new Utilitaires();

// abertura do ficheiro de dados dos contribuintes
$data = fopen($DATA, "r");
if (!$data) {
  print "Impossible d'ouvrir en lecture le fichier des données [$DATA]\n";
  exit;
}

// abertura do ficheiro de resultados
$résultats = fopen($RESULTATS, "w");
if (!$résultats) {
  print "Impossible de créer le fichier des résultats [$RESULTATS]\n";
  exit;
}

// processa-se a linha atual do ficheiro de dados dos contribuintes
while ($ligne = fgets($data, 100)) {
  // remove-se a eventual marca de fim de linha
  $ligne = $u->cutNewLineChar($ligne);
  // recuperam-se os 3 campos «casado:filhos:salário» que formam $ligne
  list($marié, $enfants, $salaire) = explode(",", $ligne);
  // calcula-se o imposto
  list($erreur, $impôt) = calculerImpot($HOTE, $PORT, $urlServeur, $cookie, array($marié, $enfants, $salaire));
  // regista-se o resultado
  $résultat = $erreur ? "$marié:$enfants:$salaire:$erreur" : "$marié:$enfants:$salaire:$impôt";
  fputs($résultats, "$résultat\n");
  // dado seguinte
}
// fecha-se os ficheiros
fclose($data);
fclose($résultats);

// fim
print "Terminé...\n";
exit;

function calculerImpot($HOTE, $PORT, $urlServeur, &$cookie, $params) {
  // liga o cliente a ($HOTE,$PORT,$urlServeur)
  // envia o cookie $cookie se este não estiver vazio. $cookie é passado por referência
  // envia $params para o servidor
  // processa a única linha devolvida pelo servidor
  
  // abertura de uma ligação na porta 80 de $HOTE
  $connexion = fsockopen($HOTE, $PORT);
  // erro?
  if (!$connexion)
    return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
  // os cabeçalhos (headers) do protocolo HTTP devem terminar com uma linha vazia
  // POST
  fputs($connexion, "POST $urlServeur HTTP/1.1\n");
  // Host
  fputs($connexion, "Host: localhost\n");
  // Conexão
  fputs($connexion, "Connection: close\n");
// o cookie é enviado se não estiver vazio
  if ($cookie) {
    fputs($connexion, "Cookie: $cookie\n");
  }//if
  // agora envia-se a instrução do cliente após a ter codificado
  $infos = "params=" . urlencode(implode(",", $params));
  // indica-se que tipo de informações serão enviadas
  fputs($connexion, "Content-type: application/x-www-form-urlencoded\n");
  // envia-se o tamanho (número de caracteres) das informações que vão ser enviadas
  fputs($connexion, "Content-length: " . strlen($infos) . "\n");
  // envia-se uma linha em branco
  fputs($connexion, "\n");
  // envia-se a informação
  fputs($connexion, $infos);
  // exibe-se a resposta do servidor web
  // e certifica-se de que se recupera o eventual cookie
  while ($ligne = fgets($connexion, 1000)) {
    // cookie - apenas na primeira resposta
    if (!$cookie) {
      if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
        $cookie = $champs[1];
      }//if
    }
    // assim que houver uma linha vazia, a resposta HTTP está concluída
    if (trim($ligne) == "") {
      break;
    }
  }//enquanto
  // leitura da linha do resultado
  $ligne = fgets($connexion, 1000);
  // encerra-se a ligação
  fclose($connexion);
  // cálculo do resultado
  $erreur="";
  $impôt="";
  if (preg_match("/^<erreur>(.*?)<\/erreur>\s*$/", $ligne, $champs)) {
    $erreur = $champs[1];
  } else {
    if (preg_match("/^<impot>(.*?)<\/impot>\s*$/", $ligne, $champs)) {
      $impôt = $champs[1];
    }else{
      $erreur="résultat du serveur non exploitable";
    }
  }
  // retorno
  return array($erreur, $impôt);
}

Comentários

O código do script do cliente retoma elementos já abordados:

  • linhas 9-15: a classe [Utilitaires] foi apresentada na versão 3, parágrafo 6
  • linhas 17-68: o programa principal é análogo ao da versão 1, parágrafo 4.2. A única diferença reside no cálculo do imposto, na linha 56.
  • linha 56: a função de cálculo do imposto aceita os seguintes parâmetros:
    • $HOTE, $PORT, $urlServeur: permitem ligar-se ao serviço web
    • $cookie: é o cookie de sessão. Este parâmetro é passado por referência. O seu valor é definido pela função de cálculo do imposto. Na primeira chamada, não tem valor. Posteriormente, passa a ter um.
    • array($marié, $enfants, $salaire): representa uma linha do ficheiro [data.txt]

A função de cálculo do imposto devolve um tabuleiro com dois resultados ($erreur, $impôt), em que $erreur é uma eventual mensagem de erro e $impôt o montante do imposto.

  • linhas 70 – 134: trata-se de um cliente HTTP clássico, como já vimos muitas vezes. Destacam-se os seguintes pontos:
  • linha 83: os parâmetros ($marié, $enfants, $salaire) são transmitidos ao servidor por um POST
  • linhas 89-91: se o cliente tiver um identificador de sessão, envia-o ao servidor
  • linha 93: criação do parâmetro params
  • linha 101: envio do parâmetro params
  • linhas 104-115: o cliente lê todos os cabeçalhos HTTP enviados pelo servidor até encontrar a linha vazia que marca o fim dos cabeçalhos. Aproveita para recuperar o identificador de sessão na resposta à sua primeira solicitação.
  • linhas 123-125: processa-se uma eventual linha com o formato <erreur>message</erreur>
  • linhas 126-128: faz-se o mesmo com uma eventual linha do tipo <impot>montant</impot>
  • linha 133: apresenta-se o resultado

11.2. O serviço web de cálculo do imposto

Estamos aqui a analisar os três scripts que compõem o servidor:

O projeto NetBeans correspondente é o seguinte:

Em [1], o servidor é composto pelos seguintes scripts PHP:

  • [impots_05_entites] contém as classes utilizadas pelo servidor
  • [impots_05_dao] contém as classes e interfaces da camada [dao]
  • [impots_05_metier] contém as classes e interfaces da camada [metier]
  • [impots_05_web] contém as classes e interfaces da camada [dao]

Começamos por apresentar duas classes utilizadas pelas diferentes camadas do serviço web.

11.2.1. As entidades do serviço web (impots_05_entites)

A base de dados MySQL [dbimpots] possui uma tabela [impots] que contém os dados necessários para o cálculo do imposto [1]:

Iremos armazenar os dados das tabelas MySQL e [impots] numa matriz de objetos Tranche, em que Tranche é a seguinte classe:


<?php

// uma faixa de imposto
class Tranche {

  // campos privados
  private $limite;
  private $coeffR;
  private $coeffN;

  // getters e setters
  public function getLimite() {
    return $this->limite;
  }

  public function setLimite($limite) {
    $this->limite = $limite;
  }

  public function getCoeffR() {
    return $this->coeffR;
  }

  public function setCoeffR($coeffR) {
    $this->coeffR = $coeffR;
  }

  public function getCoeffN() {
    return $this->coeffN;
  }

  public function setCoeffN($coeffN) {
    $this->coeffN = $coeffN;
  }

  // construtor
  public function __construct($limite, $coeffR, $coeffN) {
    $this->setLimite($limite);
    $this->setCoeffR($coeffR);
    $this->setCoeffN($coeffN);
  }

  // toString
  public function __toString(){
    return "[$this->limite,$this->coeffR,$this->coeffN]";
  }
}

Os campos privados [$limite, $coeffR, $coeffN] servirão para armazenar as colunas [limites, coeffR, coeffN] de uma linha da tabela MySQL [impots].

Além disso, o código do servidor utilizará uma exceção própria, a classe ImpotsException:

1
2
3
4
5
6
7
class ImpotsException extends Exception {

  public function __construct($message, $code=0) {
    parent::__construct($message, $code);
  }

}
  • linha 1: a classe [ImpotsException] deriva da classe [Exception] predefinida em PHP 5
  • linha 3: o construtor da classe [ImpotsException] aceita dois parâmetros:
    • $message: uma mensagem de erro
    • $code: um código de erro

11.2.2. A camada [dao] (impots_05_dao)

A camada [dao] assegura o acesso aos dados da base de dados:

A camada [dao] apresenta a seguinte interface:

1
2
3
4
5
6
// interface DAO
interface IImpotsDao {

// retorna um array de entidades Tranche
  function getData();
}

A interface IImpotsDao expõe apenas a função getData. Esta função insere, numa matriz de objetos Tranche, as diferentes linhas da tabela MySQL [dbimpots.impots].

A classe de implementação é a seguinte:


<?php

// camada DAO
// dependências
require_once "impots_05_entites.php";
// constantes
define("TABLE", "impots");

// -----------------------------------------------------------------
// implementação abstrata
abstract class ImpotsDaoWithPdo implements IImpotsDao {

// campos privados
  private $dsn;
  private $user;
  private $passwd;
  private $tranches;

// getters e setters
  public function getDsn() {
    return $this->dsn;
  }

  public function setDsn($dsn) {
    $this->dsn = $dsn;
  }

  public function getUser() {
    return $this->user;
  }

  public function setUser($user) {
    $this->user = $user;
  }

  public function getPasswd() {
    return $this->passwd;
  }

  public function setPasswd($passwd) {
    $this->passwd = $passwd;
  }

  // construtor
  public function __construct($dsn, $user, $passwd) {
    // regista-se os parâmetros
    $this->setDsn($dsn);
    $this->setUser($user);
    $this->setPasswd($passwd);
    // recuperam-se os dados do SGBD
    // liga ($user, $pwd) à base de dados $dsn
    try {
      // ligação
      $connexion = new PDO($dsn, $user, $passwd, array(PDO::ATTR_PERSISTENT => true));
      // leitura da tabela $TABLE
      $requête = "select limites,coeffR,coeffN from " . TABLE;
      // executa a consulta $requête na ligação $connexion
      $statement = $connexion->prepare($requête);
      $statement->execute();
      // análise do resultado da consulta
      while ($colonnes = $statement->fetch()) {
        $this->tranches[] = new Tranche($colonnes[0], $colonnes[1], $colonnes[2]);
      }
      // desligamento
      $connexion=NULL;
    } catch (PDOException $e) {
      // retorno com erro
      throw new ImpotsException($e->getMessage(), 1);
    }
  }

  public function getData(){
    return $this->tranches;
  }
}
  • linha 5: a implementação da interface [IImpotsDao] necessita das classes definidas no script [impots_05_entites].
  • linha 11: definição de uma classe abstrata. Uma classe abstrata é uma classe que não pode ser instanciada. Uma classe abstrata tem de ser obrigatoriamente derivada para poder ser instanciada. Uma classe pode ser declarada abstrata porque não é possível instanciá-la (alguns dos seus métodos não estão definidos) ou porque não se pretende instanciá-la. Neste caso, não se pretende instanciar a classe [ImpotsDaoWithPdo]. Serão instanciadas classes derivadas.
  • linha 11: a classe [ImpotsDaoWithPdo] implementa a interface [IImpotsDao]. Por conseguinte, deve definir o método getData. Este método encontra-se nas linhas 72-74.
  • linha 14: $dsn (Data Source Name) é uma cadeia de caracteres que identifica de forma única o SGBD e a base de dados utilizada.
  • linha 15: $user identifica o utilizador que se liga à base de dados
  • linha 16: $passwd é a palavra-passe do utilizador anterior
  • linha 17: $tranches é a matriz de objetos Tranche na qual serão armazenadas as tabelas MySQL e [dbimpots.impots].
  • linhas 45-70: o construtor da classe. Este código já foi abordado na versão 4, parágrafo 8.2. Note-se que a construção do objeto [ImpotsDaoWithPdo] pode falhar. Nesse caso, é lançada uma exceção do tipo [ImpotsException].
  • linhas 72-74: o método [getData] da interface [IImpotsDao].

A classe [ImpotsDaoWithPdo] é compatível com qualquer SGBD. O construtor da classe, na linha 45, exige que se conheça o Data Source Name da base de dados. Esta cadeia de caracteres depende do SGBD utilizado. Optou-se por não obrigar o utilizador da classe a conhecer esse Data Source Name. Para cada SGBD, haverá uma classe específica derivada de [ImpotsDaoWithPdo]. Para o SGBD MySQL, será a seguinte classe:


class ImpotsDaoWithMySQL extends ImpotsDaoWithPdo {

  public function __construct($host, $port, $base, $user, $passwd) {
    parent::__construct("mysql:host=$host;dbname=$base;port=$port", $user, $passwd);
  }

}
  • Na linha 3, o fabricante não solicita o Data Source Name, mas apenas o nome do servidor do SGBD ($host), a sua porta de escuta ($port) e o nome da base de dados ($base).
  • Na linha 4, o Data Source Name da base de dados MySQL é criado e utilizado para chamar o construtor da classe pai.

Note-se que, para se adaptar a outro SGBD, basta escrever a classe derivada de [ImpotsDaoWithPdo] que for adequada. Trata-se, em cada caso, de construir o nome da fonte de dados específico do SGBD utilizado.

11.2.3. A camada [métier] (impots_05_metier)

A camada [metier] contém a lógica de cálculo do imposto:

A camada [métier] apresenta a seguinte interface:


<?php

// interface de negócio
interface IImpotsMetier {
  public function calculerImpot($marié, $enfants, $salaire);
}

A interface [IImpotsMetier] expõe apenas um método, o método [calculerImpot], que permite calcular o imposto de um contribuinte a partir dos seguintes parâmetros:

  • $marié: cadeia de caracteres «sim»/«não», consoante o contribuinte seja casado ou não
  • $enfants: o número de filhos do contribuinte
  • $salaire: o seu salário

É a camada [web] que lhe fornecerá esses parâmetros.

A implementação da interface [IImpotsMetier] é a seguinte:


// dependências
require_once "impots_05_dao.php";

// ------------------------------------------------------------------
// classe de implementação
class ImpotsMetier implements IImpotsMetier {

  // camada DAO
  private $dao;
   // matriz de objetos [Tranche]
  private $data;

  // getter e setter
  public function getDao() {
    return $this->dao;
  }

  public function setDao($dao) {
    $this->dao = $dao;
  }

  public function setData($data){
    $this->data=$data;
  }
  
  public function __construct($dao) {
    // recuperam-se os dados necessários para o cálculo do imposto
    $this->setDao($dao);
    $this->setData($this->dao->getData());
  }

  public function calculerImpot($marié, $enfants, $salaire) {
    // $marié: sim, não
    // $enfants: número de filhos
    // $salaire: salário anual
    // número de quotas
    $marié = strtolower($marié);
    if ($marié == "oui")
      $nbParts = $enfants / 2 + 2;
    else
      $nbParts=$enfants / 2 + 1;
    // mais 1/2 quota se houver pelo menos 3 filhos
    if ($enfants >= 3)
      $nbParts+=0.5;
    // rendimento tributável
    $revenuImposable = 0.72 * $salaire;
    // quociente familiar
    $quotient = $revenuImposable / $nbParts;
    // é colocado no final da tabela de limites para interromper o ciclo seguinte
    $N = count($this->data);
    $this->data[$N - 1]->setLimite($quotient);
    // cálculo do imposto
    $i = 0;
    while ($i < $N and $quotient > $this->data[$i]->getLimite()) {
      $i++;
    }
    // uma vez que se colocou $quotient no final da tabela $limites, o ciclo anterior
    // não pode ultrapassar os limites da tabela $limites
    // agora podemos calcular o imposto
    return floor($revenuImposable * $this->data[$i]->getCoeffR() - $nbParts * $this->data[$i]->getCoeffN());
  }

}
  • linha 2: a camada [métier] necessita das classes da camada [dao] e das entidades (Tranche, ImpotsException).
  • linha 6: a classe [ImpotsMetier] implementa a interface [IimpotsMetier].
  • linhas 9-11: os campos privados da classe:
    • $dao: referência à camada [dao]
    • $data: tabela de objetos do tipo [Tranche] fornecida pela camada [dao]
  • linhas 26-30: o construtor da classe inicializa os dois campos anteriores. Recebe como parâmetro uma referência à camada [dao].
  • linhas 32-61: implementação do método [calculerImpot] da interface [IimpotsMetier]. Este método já existia desde a versão 1 (parágrafo 4.2).

11.2.4. A camada [web] (impots_05_web)

A camada [metier] contém a lógica de cálculo do imposto:

A camada [web] é constituída pelo serviço web que responde aos clientes web. Recorde-se que estes enviam um pedido ao serviço web, enviando o seguinte parâmetro: params=marié,enfants,salaire. Trata-se de um serviço web tal como os que construímos nos parágrafos anteriores. O seu código é o seguinte:


<?php

// camada de negócio
require_once "impots_05_metier.php";

// gestão de erros
ini_set("display_errors", "off");

// cabeçalho UTF-8
header("Content-Type: text/plain; charset=utf-8");

// ------------------------------------------------------------------------------
// o serviço web dos impostos
// definição de constantes
$HOTE = "localhost";
$PORT = 3306;
$BASE = "dbimpots";
$USER = "root";
$PWD = "";
// os dados necessários para o cálculo do imposto foram inseridos na tabela MySQL IMPOTS
// pertencente à base de dados $BASE. A tabela tem a seguinte estrutura
// limites decimal(10,2), coeffR decimal(6,2), coeffN decimal(10,2)
// os parâmetros dos contribuintes (estado civil, número de filhos, salário anual)
// são enviados pelo cliente no formato params=estado civil, número de filhos, salário anual
// os resultados (estado civil, número de filhos, salário anual, imposto a pagar) são devolvidos ao cliente
// na forma <impot>impot</impot>
// ou na forma <erro>erro</erro>, se os parâmetros forem inválidos

// é recuperada a camada [métier] na sessão
session_start();
if (!isset($_SESSION['metier'])) {
// instanciação da camada [dao] e da camada [métier]
  try {
    $_SESSION['metier'] = new ImpotsMetier(new ImpotsDaoWithMySQL($HOTE, $PORT, $BASE, $USER, $PWD));
  } catch (ImpotsException $ie) {
    print "<erreur>Erreur : " . utf8_encode($ie->getMessage() . "</erreur>");
    exit;
  }
}
$metier = $_SESSION['metier'];

// recupera-se a linha enviada pelo cliente
$params = utf8_encode(htmlspecialchars(strtolower(trim($_POST['params']))));
$items = explode(",", $params);
// só deve haver 3 parâmetros
if (count($items) != 3) {
  print "<erreur>[$params] : nombre de paramètres invalides</erreur>\n";
  exit;
}//se
// o primeiro parâmetro (estado civil) deve ser sim/não
$marié = trim($items[0]);
if ($marié != "oui" and $marié != "non") {
  print "<erreur>[$params] : 1er paramètre invalide</erreur>\n";
  exit;
}//se
// o segundo parâmetro (número de filhos) deve ser um número inteiro
if (!preg_match("/^\s*(\d+)\s*$/", $items[1], $champs)) {
  print "<erreur>[$params] : 2ième paramètre invalide</erreur>\n";
  exit;
}//se
$enfants = $champs[1];
// o terceiro parâmetro (salário) deve ser um número inteiro
if (!preg_match("/^\s*(\d+)\s*$/", $items[2], $champs)) {
  print "<erreur>[$params] : 3ième paramètre invalide</erreur>\n";
  exit;
}//se
$salaire = $champs[1];
// calcula-se o imposto
$impôt = $metier->calculerImpot($marié, $enfants, $salaire);
// retorna-se o resultado
print "<impot>$impôt</impot>\n";
// fim
exit;
  • linha 4: a camada [web] necessita das classes da camada [métier]
  • linhas 30-40: a referência à camada [métier] é colocada na sessão. Se nos lembrarmos de que esta camada [métier] tem uma referência à camada [dao] e que esta última armazena os dados da SGBD, compreendemos então que:
    • que a primeira solicitação do cliente irá provocar um acesso à camada SGBD
    • que as solicitações seguintes do mesmo cliente irão utilizar os dados armazenados pela camada [dao]. Não há, portanto, acesso à camada SGBD.
  • linha 34: construção de uma camada [métier] que funciona com uma camada [dao] implementada para o SGBD MySQL
  • linhas 35-37: gestão de um eventual erro na operação anterior. Neste caso, é enviada uma linha <erreur>message</erreur> ao cliente.
  • linha 43: recupera-se o parâmetro «params» que foi enviado pelo cliente.
  • linhas 46-49: verifica-se o número de informações encontradas em «params»
  • linhas 51-55: verifica-se a validade da primeira informação
  • linhas 56-60: o mesmo para a segunda
  • linhas 62-66: o mesmo se aplica à terceira
  • linha 69: é a camada [métier] que calcula o imposto.
  • linha 71: envio do resultado ao cliente

Resultados

Recorde-se que o cliente [client_impots_web_05] utiliza o seguinte ficheiro [data.txt]:

oui,2,200000
non,2,200000
oui,3,200000
non,3,200000
oui,5,50000
non,0,3000000

A partir destas linhas (estado civil, filhos, salário), o cliente consulta o servidor de cálculo de impostos e regista os resultados no ficheiro de texto [resultats.txt]. Após a execução do cliente, o conteúdo deste ficheiro é o seguinte:

oui:2:200000:22504
non:2:200000:33388
oui:3:200000:16400
non:3:200000:22504
oui:5:50000:0
non:0:3000000:1354938

onde cada linha tem o seguinte formato (casado, filhos, salário, imposto calculado).