11. Exercício Prático – Versão 4
A aplicação de cálculo de impostos irá implementar a seguinte arquitetura em camadas:

Iremos reutilizar os elementos da versão 3 da secção associada, modificando-os para os adaptar à nova arquitetura da aplicação. Isto é por vezes designado por «refatoração». Aqui, partimos do princípio de que os dados necessários à aplicação estão armazenados em ficheiros de texto. A camada [Dao] irá gerir as interações com estes ficheiros.
11.1. Árvore de scripts

11.2. Objetos trocados entre camadas
Iremos manter certos objetos da versão 3. Listamo-los aqui como lembrete.
A exceção [ExceptionImpots] é a exceção que a camada [Dao] irá lançar quando encontrar um problema, seja com o acesso aos dados ou com a natureza dos dados (dados incorretos).
<?php
// namespace
namespace Application;
class ExceptionImpots extends \RuntimeException {
public function __construct(string $message, int $code=0) {
parent::__construct($message, $code);
}
}
A classe [Utilities] contém métodos úteis para gerir ficheiros de texto (neste caso, um único método):
<?php
// namespace
namespace Application;
// a class of utility functions
abstract class Utilitaires {
public static function cutNewLinechar(string $ligne): string {
// delete the end-of-line mark from $ligne if it exists
$longueur = strlen($ligne); // line length
while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
$ligne = substr($ligne, 0, $longueur - 1);
$longueur--;
}
// end - return the line
return($ligne);
}
}
A classe [TaxAdminData] é a classe que encapsula os dados de administração fiscal:
<?php
namespace Application;
class TaxAdminData {
// tax brackets
private $limites;
private $coeffR;
private $coeffN;
// tax calculation constants
private $plafondQfDemiPart;
private $plafondRevenusCelibatairePourReduction;
private $plafondRevenusCouplePourReduction;
private $valeurReducDemiPart;
private $plafondDecoteCelibataire;
private $plafondDecoteCouple;
private $plafondImpotCouplePourDecote;
private $plafondImpotCelibatairePourDecote;
private $abattementDixPourcentMax;
private $abattementDixPourcentMin;
// initialization
public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
// retrieve the contents of the tax data file
$fileContents = \file_get_contents($taxAdminDataFilename);
…
// we return the object
return $this;
}
private function check($value): \stdClass {
…
return $result;
}
// toString
public function __toString() {
// object's Json string
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
// getters and setters
public function getLimites() {
return $this->limites;
}
…
public function setLimites($limites) {
$this->limites = $limites;
return $this;
}
…
}
Adicionamos uma nova classe [TaxPayerData] que encapsula os dados gravados no ficheiro de resultados:
<?php
// namespace
namespace Application;
// data class
class TaxPayerData {
// data required to calculate the taxpayer's tax liability
private $marié;
private $enfants;
private $salaire;
// tax calculation results
private $montant;
private $surcôte;
private $décôte;
private $réduction;
private $taux;
// setter
public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel) : TaxPayerData{
// taxpayer data required for tax calculation
$this->marié = $marié;
$this->enfants = $nbEnfants;
$this->salaire = $salaireAnnuel;
// initialize the object
return $this;
}
// getters and setters
public function getMarié() {
return $this->marié;
}
…
public function setMarié($marié) {
$this->marié = $marié;
return $this;
}
…
// toString
public function __toString() {
// object's Json string
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
}
Nota: Utilize a geração automática de código para gerar o construtor, os getters e os setters (consulte a secção indicada no link). Note que os setters são «fluentes».
11.3. A camada [DAO]
Aqui, estamos a concentrar-nos na camada [1] da nossa aplicação:

11.3.1. A interface [InterfaceDao]
A interface para a camada [DAO] será a seguinte [InterfaceDao.php]:
<?php
// namespace
namespace Application;
interface InterfaceDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// reading tax data (tax brackets)
public function getTaxAdminData(): TaxAdminData;
// recording results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Comentários
- Os requisitos são os seguintes:
- Os dados do contribuinte são armazenados num ficheiro de texto;
- os resultados do cálculo do imposto são guardados num ficheiro de texto;
- Quaisquer erros são guardados num ficheiro de texto;
- não se sabe em que formato os dados da autoridade fiscal estão disponíveis. Para cada novo formato, a interface [InterfaceDao] deve ser implementada por uma nova classe;
- os métodos da interface que encontram um erro fatal ao aceder aos dados devem lançar uma exceção do tipo [TaxException];
- linha 9: o método que recupera os dados do contribuinte [estado civil, número de filhos, salário anual];
- o primeiro parâmetro é o nome do ficheiro de texto que contém estes dados;
- o segundo parâmetro é o nome do ficheiro de texto no qual se devem registar quaisquer erros encontrados;
- linha 12: o método que recupera dados da autoridade fiscal. Aqui não são passados parâmetros porque não sabemos como os dados estão armazenados;
- linha 15: o método utilizado para guardar os resultados do cálculo do imposto num ficheiro de texto, cujo nome é passado como parâmetro;
Ao escrever a interface [InterfaceDao], sabemos que haverá diferentes formas de implementar o método [getTaxAdminData], dependendo de como os dados da administração fiscal estão armazenados. A interface [InterfaceDao] será, portanto, implementada por diferentes classes, cada uma a lidar com um método de armazenamento específico para estes dados (matrizes, ficheiros de texto, bases de dados, serviços web). Estas classes derivadas partilharão, no entanto, código comum, especificamente a implementação dos métodos [getTaxPayersData] e [saveResults]. Sabemos que este caso de utilização pode ser implementado de duas formas (ver parágrafo em destaque):
- Criamos uma classe abstrata C que contém o código comum às classes derivadas. A classe C implementa a interface I, mas certos métodos que devem ser declarados nas classes derivadas são declarados como abstratos na classe C e, portanto, a própria classe C é abstrata. Criamos então as classes C1 e C2 derivadas de C, cada uma das quais implementa os métodos não definidos (abstratos) da sua classe pai C à sua maneira;
- criamos uma característica T que é quase idêntica à classe abstrata C da solução anterior. Esta característica não implementa a interface I porque, sintaticamente, não o pode fazer. Criamos então as classes C1 e C2 que implementam a interface I e utilizam a característica T. Tudo o que resta a estas classes é implementar os métodos da interface I que não são implementados pela característica T que importam;
Para este exemplo, utilizaremos aqui um trait [TraitDao].
11.3.2. O trait [TraitDao]
O código para o trait [TraitDao] é o seguinte [TraitDao.php]:
<?php
// namespace
namespace Application;
trait TraitDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// taxpayer data table
$taxPayersData = [];
// error table
$errors = [];
// many errors can occur when managing files
try {
// reading user data
// each line has the form marital status, number of children, annual salary
$taxPayersFile = fopen($taxPayersFilename, "r");
if (!$taxPayersFile) {
throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
}
// the current line of the user data file is used
// in the form of marital status, number of children, annual salary
$num = 1; // n° current line
$nbErreurs = 0; // number of errors encountered
while ($ligne = fgets($taxPayersFile, 100)) {
// empty lines are neglected
$ligne = trim($ligne);
if (strlen($ligne) == 0) {
// next line
$num++;
// we loop again
continue;
}
// remove any end-of-line marker
$ligne = Utilitaires::cutNewLineChar($ligne);
// we retrieve the 3 fields married:children:salary which form $ligne
list($marié, $enfants, $salaire) = explode(",", $ligne);
// we check them
// marital status must be yes or no
$marié = trim(strtolower($marié));
$erreur = ($marié !== "oui" and $marié !== "non");
if (!$erreur) {
// the number of children must be an integer
$enfants = trim($enfants);
if (!preg_match("/^\d+$/", $enfants)) {
$erreur = TRUE;
} else {
$enfants = (int) $enfants;
}
}
if (!$erreur) {
// the salary is a whole number without the euro cents
$salaire = trim($salaire);
if (!preg_match("/^\d+$/", $salaire)) {
$erreur = TRUE;
} else {
$salaire = (int) $salaire;
}
}
// mistake?
if ($erreur) {
$errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
$nbErreurs++;
} else {
// memorize information
$taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
}
// next line
$num++;
}
// are we at the end of the file?
if (!feof($taxPayersFile)) {
// we're out of the loop on a read error
throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
} else {
// we're out of the loop at the end-of-file mark
// save errors in a text file
$this->saveString($errorsFilename, implode("\n", $errors));
// function result
return $taxPayersData;
}
} finally {
// close the file if it is open
if ($taxPayersFile) {
fclose($taxPayersFile);
}
}
}
// recording results
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// save table [$taxPayersData] in text file [$resultsFileName]
// if text file [$resultsFileName] does not exist, it is created
$this->saveString($resultsFilename, implode("\n", $taxPayersData));
}
// saving table results in a text file
private function saveString(string $fileName, string $data): void {
// save table [$data] in text file [$fileName]
// if text file [$fileName] does not exist, it is created
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
Comentários
- linha 6: aqui definimos uma característica, não uma classe;
- linhas 9–89: O método [getTaxPayersData] implementa o método com o mesmo nome da interface [InterfaceDao]. Ele recupera dados dos contribuintes [estado civil, número de filhos, rendimento anual ] de um ficheiro de texto denominado [$taxPayersFilename]. Ele devolve estes dados como uma matriz [$taxPayersData] de elementos do tipo [TaxPayerData] (linhas 67, 81);
- o método [getTaxPayersData] é muito semelhante ao método [AbstractBaseImpots::executeBatchImpots] descrito na secção referenciada, com as seguintes diferenças:
- o método [getTaxPayersData] apenas recupera dados dos contribuintes. Não realiza quaisquer cálculos fiscais. Aqui, essa é a função da camada [business];
- Tal como o método [executeBatchImpots], reporta erros. Aqui, os erros são primeiro armazenados numa matriz [$errors] (linha 13), que é depois guardada num ficheiro de texto no final do processo (linha 79). Dependendo da situação, esta matriz pode ou não estar vazia;
- no caso de um erro fatal, é lançada uma exceção [ExceptionImpots] (linhas 20, 75);
- linha 73: repare no processamento realizado ao sair do ciclo nas linhas 26–71. De facto, a função [fgets] tem a desvantagem de devolver o valor booleano FALSE tanto quando a leitura das linhas encontra o marcador de fim de ficheiro como quando a leitura falha devido a um erro. Para distinguir entre os dois casos, verificamos se chegámos ao fim do ficheiro utilizando a função [feof]. Se não tivermos chegado ao fim do ficheiro, isso significa que ocorreu um erro e, nesse caso, lançamos uma exceção;
- linhas 83–88: o bloco [finally] é executado independentemente de ter ocorrido uma exceção durante o processamento do ficheiro;
- linha 85: se o ficheiro tiver sido aberto, então o «handle» do ficheiro [$taxPayersFile] tem o valor booleano TRUE; caso contrário, é FALSE;
- linhas 99–105: o método privado [saveString] utilizado na linha 79 para guardar a matriz de erros num ficheiro de texto;
- linha 99: o método [saveString] recebe dois parâmetros:
- [string $filename], que é o nome do ficheiro de texto utilizado para guardar os dados;
- [string $data], que é a string a ser guardada no ficheiro de texto. Esta string será uma sequência de linhas terminadas pelo caractere de nova linha \n;
- linha 102: a função PHP [file_puts_contents] escreve uma string num ficheiro de texto. Abre o ficheiro, escreve a string nele e fecha o ficheiro. Retorna FALSE se ocorrer um erro;
- linha 103: se ocorrer um erro, é lançada uma exceção;
- linhas 92–96: implementação do método [saveResults] da interface [InterfaceDao]. O método privado [saveString] é utilizado novamente. Aqui, o segundo parâmetro de [saveString] é uma cadeia de caracteres construída a partir da matriz [$taxPayersData], cujos elementos são do tipo [TaxPayerData]. Poder-se-á questionar qual será o resultado da operação:
implode("\n", $taxPayersData)
Definimos o seguinte método [__toString] na classe [TaxPayerData] (ver secção em link):
public function __toString() {
// chaîne Json de l'objet
return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
}
A operação
implode("\n", $taxPayersData)
irá concatenar cada elemento da matriz [$taxPayersData] — convertido numa cadeia de caracteres pelo seu método [__toString] — com o caractere de nova linha \n. Isto resultará numa cadeia de caracteres com o seguinte formato:
json1\njson2\n…
Conclusão
A característica [TraitDao] implementou dois dos métodos da interface [InterfaceDao], [getTaxPayersData] e [saveResults]:
<?php
// namespace
namespace Application;
interface InterfaceDao {
// reading taxpayer data
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// reading tax data (tax brackets)
public function getTaxAdminData(): TaxAdminData;
// recording results
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
Ainda precisamos de implementar o método [getTaxAdminData], que recupera dados da administração fiscal.
11.3.3. A classe [ImpotsWithTaxAdminDataInJsonFile]
A classe [ImpotsWithTaxAdminDataInJsonFile] implementa a interface [InterfaceDao] da seguinte forma:
<?php
// namespace
namespace Application;
// definition of a ImpotsWithDataInFile class
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
// use of a line
use TraitDao;
// the TaxAdminData object containing tax bracket data
private $taxAdminData;
// the manufacturer
public function __construct(string $taxAdminDataFilename) {
// we want to initialize the [$this->taxAdminData] attribute
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// returns data for tax calculation
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Comentários
- linha 7: a classe [ImpotsWithTaxAdminDataInJsonFile] implementa a interface [InterfaceDao];
- linha 9: a classe [ImpotsWithTaxAdminDataInJsonFile] utiliza o traço [traitDao], que, como sabemos, implementa os métodos [getTaxPayersData] e [saveResults] da interface [InterfaceDao]. Resta, portanto, que a classe [ImpotsWithTaxAdminDataInJsonFile] implemente o método [getTaxAdminData], que recupera dados da administração fiscal;
- Linha 11: o atributo do tipo [TaxAdminData] devolvido pelo método [getTaxAdminData] nas linhas 20–22. Este atributo é inicializado pelo construtor nas linhas 14–17;
Concluímos agora a camada [DAO] da nossa aplicação: temos uma classe que implementa totalmente a interface [InterfaceDao] que definimos. Podemos agora passar para a camada [business].
11.4. A camada [business]
Vamos agora implementar a camada [2] da nossa arquitetura:

11.4.1. A interface [InterfaceMétier]
A interface para a camada [business] será a seguinte:
<?php
// namespace
namespace Application;
interface InterfaceMetier {
// 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;
}
Comentários
- linha 9: a interface [BusinessInterface] pode calcular o valor do imposto para um contribuinte individual, desde que lhe sejam fornecidas as seguintes informações: estado civil, número de filhos, salário anual. O método [calculateTax] não utiliza a camada [DAO], pelo que não lança exceções;
- Linha 9: A interface [BusinessInterface] também pode calcular o valor do imposto para um grupo de contribuintes cujos dados estão reunidos no ficheiro de texto denominado [$taxPayersFileName]. Ela grava os resultados num ficheiro de texto denominado [$resultsFileName]. O método [executeBatchImpots] deve comunicar com a camada [dao], que gere o acesso ao sistema de ficheiros. As exceções podem então ser propagadas a partir da camada [dao], que o método [executeBatchImpots] não irá capturar: permitirá que se propaguem para o script principal. Os erros não fatais são registados no ficheiro de texto denominado [$errorsFileName];
- linha 9: o método [calculateTax] é um método puramente [business]. Não se preocupa com a origem dos dados que utiliza;
- linha 12: o método [executeBatchImpots] irá interagir com a camada [dao] para ler e escrever dados em ficheiros de texto. Irá chamar repetidamente o método de negócio [calculerImpot];
11.4.2. A classe [Business]
A classe [Metier] implementa a interface [InterfaceMetier] da seguinte forma:
<?php
// namespace
namespace Application;
class Metier implements InterfaceMetier {
// dao layer
private $dao;
// tax administration data
private $taxAdminData;
//---------------------------------------------
// setter couche [dao]
public function setDao(InterfaceDao $dao) {
$this->dao = $dao;
return $this;
}
public function __construct(InterfaceDao $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);
}
// batch mode tax calculation
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
…
// recording results
$this->dao->saveResults($resultsFileName, $results);
}
}
Comentários
- linha 6: a classe [Business] implementa a interface [BusinessInterface], ou seja, os métodos [calculateTax] (linhas 30–34) e [executeBatchTaxes] (linhas 66–70);
- linha 8: uma referência à camada [dao]. Isto é necessário para que a camada [business] saiba onde procurar quando precisar de dados externos. Este atributo será inicializado através do setter nas linhas 14–17 ou através do construtor nas linhas 19–26;
- linha 10: o objeto do tipo [TaxAdminData] que encapsula os dados de administração fiscal. Estes dados são necessários para o método de negócio [calculateTax]. Este atributo é inicializado através do construtor nas linhas 19–26;
- linhas 19–26: o construtor inicializa os dois atributos da classe:
- o atributo [$dao] é inicializado com a referência passada como parâmetro ao construtor. Note-se que o tipo deste parâmetro é o da interface [InterfaceDao], permitindo que a classe [Metier] seja inicializada por qualquer classe que implemente esta interface;
- o atributo [$taxAdminData] é inicializado chamando o método [getTaxAdminData] da camada [dao];
Concluímos que, quando os métodos [calculateTaxes] e [executeBatchTaxes] são executados, ambos os atributos [$dao] e [$taxAdminData] são inicializados.
O método [calculateTaxes] é o seguinte:
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
// $marié : oui, non
// $enfants : nombre d'enfants
// $salaire : salaire annuel
// $this->taxAdminData : données de l'administration fiscale
//
// on vérifie qu'on a bien les données de l'administration fiscale
if ($this->taxAdminData === NULL) {
$this->taxAdminData = $this->getTaxAdminData();
}
// calcul de l'impôt avec enfants
$result1 = $this->calculerImpot2($marié, $enfants, $salaire);
$impot1 = $result1["impôt"];
// calcul de l'impôt sans les enfants
if ($enfants != 0) {
$result2 = $this->calculerImpot2($marié, 0, $salaire);
$impot2 = $result2["impôt"];
// application du plafonnement du quotient familial
$plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
if ($enfants < 3) {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
$impot2 = $impot2 - $enfants * $plafonDemiPart;
} else {
// $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
$impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
}
} else {
$impot2 = $impot1;
$result2 = $result1;
}
// on prend l'impôt le plus fort
if ($impot1 > $impot2) {
$impot = $impot1;
$taux = $result1["taux"];
$surcôte = $result1["surcôte"];
} else {
$surcôte = $impot2 - $impot1 + $result2["surcôte"];
$impot = $impot2;
$taux = $result2["taux"];
}
// calcul d'une éventuelle décôte
$décôte = $this->getDecôte($marié, $salaire, $impot);
$impot -= $décôte;
// calcul d'une éventuelle réduction d'impôts
$réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
$impot -= $réduction;
// résultat
return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
}
Comentários
- Este código provém do método [AbstractBaseImpots::calculateTax] da versão 3, explicado na secção indicada no link. O mesmo se aplica aos métodos privados [calculateTax2, getDiscount, getReduction, getTaxableIncome];
O método [Metier::executeBatchImpots] é o seguinte:
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->dao->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->setMontant($result["impôt"]);
$taxPayerData->setDécôte($result["décôte"]);
$taxPayerData->setSurCôte($result["surcôte"]);
$taxPayerData->setTaux($result["taux"]);
$taxPayerData->setRéduction($result["réduction"]);
// put the result in the results table
$results [] = $taxPayerData;
}
// recording results
$this->dao->saveResults($resultsFileName, $results);
}
Comentários
- linha 1: o método deve chamar repetidamente o método [calculateTax] para cada contribuinte encontrado no ficheiro de texto denominado [$taxPayersFileName]. Deve gravar os resultados no ficheiro de texto denominado [$resultsFileName]. Os erros não fatais encontrados são registados no ficheiro de texto denominado [$errorsFileName]. O método não lança exceções por si próprio, mas permite que as exceções lançadas pela camada [dao] se propaguem;
- Linha 4: os dados dos contribuintes são solicitados à camada [dao]. Isto devolve uma matriz de elementos do tipo [TaxPayerData], que é uma classe de atributos [married, numberOfChildren, salary, amount, deduction, reduction, surcharge, rate] (ver parágrafo em link). Se ocorrer uma exceção aqui, uma vez que não é capturada por um bloco catch, propagar-se-á automaticamente de volta para o código de chamada. Isto significa que, no caso de uma exceção, a linha 6 não é executada;
- linha 6: a matriz de resultados do tipo [TaxPayerData];
- linhas 8–22: o imposto é calculado para cada elemento da matriz de contribuintes [$taxPayersData]. Para tal, é chamado o método interno [calculateTax] (linha 10);
- linhas 15–19: o resultado é utilizado para inicializar os atributos de [TaxPayerData] que ainda não tinham sido inicializados;
- linha 21: o resultado é adicionado à matriz de resultados [$results];
- linha 24: assim que o imposto for calculado para todos os contribuintes, os resultados são guardados num ficheiro de texto. A camada [dao] encarrega-se desta tarefa;
Conclusão
Em geral, a camada [business] é bastante simples de escrever, pois faz interface com a camada [DAO], que gere o acesso aos dados juntamente com o tratamento de erros associado.
11.5. O script principal
Vamos agora escrever o script para a camada [3] da nossa arquitetura:

O script principal é o seguinte [main.php]:
<?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");
// interface and class inclusion
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// definition of constants
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// creation of the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// creation of the [business] layer
$métier = new Metier($dao);
// tax calculation in batch mode
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// error is displayed
print $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;
Comentários
- Linha 24: o nome do ficheiro de dados do contribuinte;
- linha 25: o nome do ficheiro de resultados;
- linha 26: o nome do ficheiro de erros;
- linha 27: o nome do ficheiro JSON que contém os dados da autoridade fiscal;
- linha 31: criação da camada [dao];
- linha 33: criação da camada [business] com base nesta camada [dao];
- linha 35: execução do método [executeBatchImpots] da camada [business];
- linhas 36–39: vimos que a camada [business] pode lançar exceções. Estas são capturadas aqui;
11.6. Testes visuais
11.6.1. Teste n.º 1
Com o seguinte ficheiro de contribuintes [taxpayersdata.txt]:
oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000
Obtemos o seguinte ficheiro de erros [errors.txt]:
la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée
e o seguinte ficheiro de resultados [resultats.txt]:
11.6.2. Teste n.º 2
No script principal, atribuímos um nome de ficheiro que não existe ao ficheiro do contribuinte:
Os resultados apresentados na consola são os seguintes:
Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
- linha 1: avisos do interpretador PHP;
- linha 2: mensagem de erro da exceção lançada pela camada [dao];
É possível suprimir as mensagens de erro do interpretador PHP:

A linha 21 do código acima instrui o sistema a não exibir erros de PHP. Durante a fase de desenvolvimento, é necessário que estes sejam exibidos. No modo de produção, devem ser ocultados.
Os resultados da execução são então os seguintes:
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
11.7. Testes [Codeception]
Os testes visuais são muito inadequados:
- geralmente limitamo-nos a apenas alguns testes;
- podemos não prestar atenção suficiente durante esta inspeção visual, e alguns detalhes podem escapar-nos;
No mundo real do desenvolvimento profissional, os testes são elaborados por indivíduos dedicados para quem esta é a função principal. Eles esforçam-se por tornar os testes o mais abrangentes possível. Para tal, utilizam estruturas de testes.
Aqui, utilizaremos a estrutura Codeception [https://codeception.com/] porque pode ser integrada no NetBeans. É uma estrutura com uma vasta gama de capacidades. Utilizaremos apenas algumas delas. A ideia é ter uma forma rápida, após cada nova versão do exercício da aplicação, de verificar se esta funciona. A existência de testes bem-sucedidos dá ao programador confiança no código que escreveu. Este é um fator importante.
11.7.1. Instalação da estrutura [Codeception]
Tal como muitas bibliotecas PHP, o framework [Codeception] é instalado utilizando o [Composer]. Por isso, abrimos um terminal Laragon (ver link no parágrafo).
Primeiro, precisamos de instalar a estrutura de testes PHPUnit [https://phpunit.de/]. Isto porque o Codeception utiliza a estrutura PHPUnit nos bastidores:

Em seguida, instalamos o framework Codeception:

É isso. Agora vamos ver como integrar o [Codeception] no NetBeans.
11.7.2. Integrar o [Codeception] no NetBeans

- Em [1-2], aceda às propriedades do projeto;
- Em [3-4], definimos o [Codeception] como uma das estruturas de testes do projeto;


- Em [5-8], inicialize a estrutura [Codeception] para o projeto;

- Em [9], foi criada uma pasta [tests], juntamente com um ficheiro de configuração [codeception.yml] em [10-11]. O ficheiro [11] é idêntico ao ficheiro [10]. O Codeception limitou-se a criar uma pasta [Important Files] para atribuir ao ficheiro [10] uma designação especial;
- em [12-13], voltamos às propriedades do projeto;

- em [14-16], a pasta [tests] [16] é designada como a pasta de testes do projeto;
- em [16], a pasta [tests] aparece então com o novo nome [Test Files]. A presença desta pasta num projeto PHP indica que o projeto incorpora uma estrutura de testes unitários;
- vamos criar os nossos testes na pasta [unit] [17];
11.7.3. Testes para a camada [dao]

- Criaremos todos os nossos testes na pasta [unit] [1];
- Os nomes das classes de teste [Codeception] devem terminar com a palavra-chave [Test], caso contrário as classes não serão reconhecidas como classes de teste;
As nossas classes de teste [Codeception] terão o seguinte formato [https://codeception.com/docs/05-UnitTests]:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// loading the test environment
…
class DaoTest extends \Codeception\Test\Unit {
// test attributes
private $attribut1;
public function __construct() {
parent::__construct();
// test environment initialization
…
}
// tests
public function testTaxAdminData() {
// tests
$this->assertEquals($expected, $actual);
$this->assertEqualsWithDelta($expected, $actual, $delta);
$this->assertTrue($actual);
$this->assertFalse($actual);
$this->assertNull($actual);
$this->assertEmpty($actual);
$this→assertSame($expected, $actual);
…
}
}
Comentários
- linha 7: as classes de teste estarão no mesmo namespace que a aplicação em teste;
- linhas 9–10: aqui estão as instruções [require] para carregar as classes e interfaces testadas;
- linha 12: o nome da classe de teste deve terminar com a palavra-chave [Test]. Esta classe deve estender a classe [\Codeception\Test\Unit];
- linhas 16–20: o construtor permite-nos inicializar o ambiente de teste;
- linha 23: os nomes dos métodos de teste devem começar com a palavra-chave [test];
- linhas 25–31: podem ser utilizados vários métodos de teste;
A classe de teste [DaoTest] será a seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// interface and class inclusion
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// creation of the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
…
}
}
Comentários
Para criar os testes para uma versão do exercício da aplicação, utilizaremos um ambiente idêntico ao utilizado pelo script principal da versão. Para a versão 04, trata-se do seguinte script [main.php]:
<?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");
// interface and class inclusion
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// definition of constants
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";
try {
// creation of the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
// creation of the [business] layer
$métier = new Metier($dao);
// tax calculation in batch mode
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// error is displayed
print $ex->getMessage() . "\n";
}
// end
print "Terminé\n";
exit;
Para testar a camada [dao], na classe de teste:
- utilizamos o ambiente das linhas 13–27 de [main.php];
- no construtor da classe de teste, instanciamos a camada [dao] como na linha 31;
- escrevemos os métodos de teste;
Procederemos desta forma para todas as classes de teste.
Voltemos ao código completo da classe de teste:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// interface and class inclusion
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class DaoTest extends \Codeception\Test\Unit {
// TaxAdminData
private $taxAdminData;
public function __construct() {
parent::__construct();
// creation of the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// tests
public function testTaxAdminData() {
// calculation constants
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
$this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
$this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
$this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
$this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
$this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
$this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
$this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
$this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
$this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
// tax brackets
$this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
$this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
$this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
}
}
Comentários
- linhas 10–25: carregamento do ambiente necessário para testes e definição de constantes;
- linhas 31–36: construção da camada [dao] (linha 34), seguida da inicialização do atributo [$taxAdminData] (linha 29). Este atributo contém dados de administração fiscal;
- linhas 39–55: o único método de teste. Consiste em verificar se o conteúdo do atributo [$taxAdminData] corresponde ao esperado;
- linhas 41–50: verificações das constantes de cálculo de impostos;
- linhas 52–55: verificações das faixas de imposto. O método [assertSame] verifica se duas entidades PHP — neste caso, matrizes — são idênticas;
Para executar esta classe de teste, proceda da seguinte forma:

- em [1-2], execute o teste;
- [3]: a janela de resultados do teste;
- [4]: a classe de teste executada;
- [5]: os resultados. Aqui, o único método de teste foi aprovado;
- [6]: quando o teste falha, ou mais frequentemente quando nenhum teste foi executado, verifique a janela [6]. Na maioria das vezes, o ambiente de teste não conseguiu carregar, pelo que nenhum teste pôde ser executado. Os erros apresentados em [6] são os mesmos que veria ao executar um script PHP padrão;
Vejamos um exemplo de um teste com falha:
Na classe de teste, introduzimos um erro na definição de uma constante:
// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");
depois executamos o teste. O resultado é o seguinte:

Na janela [4]:

11.7.4. Testes da Camada [Business]
A classe de teste [MetierTest] segue as mesmas regras de construção que a classe [DaoTest], mas possui mais métodos de teste:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
// namespace
namespace Application;
// constants
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// interface and class inclusion
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// definition of constants
const TAXADMINDATA_FILENAME = "taxadmindata.json";
class MetierTest extends \Codeception\Test\Unit {
// business layer
private $métier;
public function __construct() {
parent::__construct();
// creation of the [dao] layer
$dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
// creation of the [business] layer
$this->métier = new Metier($dao);
}
// tests
public function test1() {
$result = $this->métier->calculerImpot("oui", 2, 55555);
$this->assertEqualsWithDelta(2815, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test2() {
$result = $this->métier->calculerImpot("oui", 2, 50000);
$this->assertEqualsWithDelta(1385, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(384, $result["décôte"], 1);
$this->assertEqualsWithDelta(347, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test3() {
$result = $this->métier->calculerImpot("oui", 3, 50000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(720, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test4() {
$result = $this->métier->calculerImpot("non", 2, 100000);
$this->assertEqualsWithDelta(19884, $result["impôt"], 1);
$this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test5() {
$result = $this->métier->calculerImpot("non", 3, 100000);
$this->assertEqualsWithDelta(16782, $result["impôt"], 1);
$this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test6() {
$result = $this->métier->calculerImpot("oui", 3, 100000);
$this->assertEqualsWithDelta(9200, $result["impôt"], 1);
$this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.3, $result["taux"]);
}
public function test7() {
$result = $this->métier->calculerImpot("oui", 5, 100000);
$this->assertEqualsWithDelta(4230, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.14, $result["taux"]);
}
public function test8() {
$result = $this->métier->calculerImpot("non", 0, 100000);
$this->assertEqualsWithDelta(22986, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
public function test9() {
$result = $this->métier->calculerImpot("oui", 2, 30000);
$this->assertEqualsWithDelta(0, $result["impôt"], 1);
$this->assertEqualsWithDelta(0, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0, $result["taux"]);
}
public function test10() {
$result = $this->métier->calculerImpot("non", 0, 200000);
$this->assertEqualsWithDelta(64210, $result["impôt"], 1);
$this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.45, $result["taux"]);
}
public function test11() {
$result = $this->métier->calculerImpot("oui", 3, 200000);
$this->assertEqualsWithDelta(42842, $result["impôt"], 1);
$this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
$this->assertEqualsWithDelta(0, $result["décôte"], 1);
$this->assertEqualsWithDelta(0, $result["réduction"], 1);
$this->assertEquals(0.41, $result["taux"]);
}
}
Comentários
- linhas 10–25: carregamento dos ficheiros que definem o ambiente de teste. Isto é igual ao da camada [dao];
- linhas 31–37: instanciação das camadas [dao] e [business];
- linhas 40–47: um teste de cálculo de impostos;
- linha 41: é realizado um cálculo de imposto específico utilizando a camada [business];
- linhas 42–46: verificação de que os resultados obtidos correspondem aos do simulador da autoridade fiscal [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm];
- linhas 23–26: são realizados testes de igualdade com uma margem de erro de 1 euro. De facto, observámos que problemas de arredondamento fizeram com que o algoritmo do documento produzisse os resultados esperados com uma margem de erro de 1 euro;
- linha 27: a taxa de imposto é calculada sem qualquer margem de erro;
- linhas 49–137: este tipo de teste é repetido 10 vezes, cada vez com uma configuração diferente do contribuinte;
Os testes produzem os seguintes resultados:

11.7.5. Testes para versões futuras
Daqui em diante, os testes para as camadas [dao] e [business] serão idênticos aos da versão 04. Apenas o ambiente de teste irá mudar. Apresentaremos, portanto, apenas este ambiente e os resultados dos testes.