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):
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:
- gravará a resposta do servidor num ficheiro de texto [resultats.txt] numa das duas formas seguintes:
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:
- 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:
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]:
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).






