Skip to content

13. Exercício prático – Versão 5

Image

Já escrevemos várias versões deste exercício. A versão mais recente utilizava uma arquitetura em camadas:

Image

A camada [dao] implementa uma interface [InterfaceDao]. Criámos uma classe que implementa esta interface:

  • [DaoImportsWithTaxAdminDataInJsonFile], que recuperava dados fiscais de um ficheiro JSON;

Iremos implementar a interface [InterfaceDao] com uma nova classe [DaoImpotsWithTaxAdminDataInDatabase], que irá recuperar dados da administração fiscal de uma base de dados MySQL.

13.1. Criação da base de dados [dbimpots-2019]

Seguindo o exemplo na secção «link», criamos uma base de dados MySQL denominada [dbimpots-2019], pertencente a [admimpots] com a palavra-passe [mdpimpots]:

Image

  • Em [1-4] acima, vemos a base de dados [dbimpots-2019], que atualmente não tem tabelas;

Image

  • No [1-5] acima, vemos que o utilizador [admimpots] tem privilégios totais na base de dados [dbimpots-2019]. O que não vemos aqui é que este utilizador tem a palavra-passe [admimpots];

Vamos agora criar a tabela [tbtranches], que conterá as faixas de imposto:

Image

  • Em [1-7], criamos uma tabela chamada [tbtranches] com 4 colunas;

Image

  • Em [3-6], definimos uma coluna chamada [id] (3), do tipo inteiro [int] (4), que será a chave primária [6] da tabela e será autoincrementada [5] pelo SGBD. Isto significa que o MySQL irá gerir os valores da chave primária durante as inserções. Atribuirá o valor 1 à chave primária da primeira inserção, depois 2 à seguinte, e assim sucessivamente;
  • em [7], o assistente oferece opções de configuração adicionais para a chave primária. Aqui, limitamo-nos a aceitar [7] os valores predefinidos;

Image

  • Em [8-16], definimos as outras três colunas da tabela:
    • [limits] (8), um número decimal (9) com 10 dígitos, incluindo 2 casas decimais (10), conterá os elementos da coluna 17 das faixas de imposto;
    • [coeffR] (11), um número decimal de 6 dígitos (12) com 2 casas decimais (13), conterá os valores da coluna 18 das faixas de imposto;
    • [coeffN] (14) do tipo número decimal (15) com 10 dígitos, incluindo 2 casas decimais (16), conterá os elementos da coluna 19 das faixas de imposto;

Após validar esta estrutura, obtemos o seguinte resultado:

Image

  • em [5], o ícone de chave indica que a coluna [id] é a chave primária. Podemos também ver que esta chave primária tem valores inteiros (6) e é gerida (auto-incrementada) pelo MySQL;

Da mesma forma que criámos a tabela [tbtranches], construímos a tabela [tbconstantes], que conterá as constantes utilizadas no cálculo de impostos:

Image

É possível exportar a estrutura da base de dados para um ficheiro de texto como uma sequência de instruções SQL:

Image

A opção [5] exporta apenas a estrutura da base de dados, não o seu conteúdo. No nosso caso, a base de dados ainda não tem qualquer conteúdo.

Image

Image

Image

A opção [11] gera o seguinte ficheiro SQL [dbimpots-2019.sql]:


-- phpMyAdmin SQL Dump
-- version 4.8.5
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Jun 30, 2019 at 01:10 PM
-- Server version: 5.7.24
-- PHP Version: 7.2.11
 
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
 
 
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
 
--
-- Database: `dbimpots-2019`
--
CREATE DATABASE IF NOT EXISTS `dbimpots-2019` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `dbimpots-2019`;
 
-- --------------------------------------------------------
 
--
-- Table structure for table `tbconstantes`
--
 
DROP TABLE IF EXISTS `tbconstantes`;
CREATE TABLE `tbconstantes` (
  `id` int(11) NOT NULL,
  `plafondQfDemiPart` decimal(10,2) NOT NULL,
  `plafondRevenusCelibatairePourReduction` decimal(10,2) NOT NULL,
  `plafondRevenusCouplePourReduction` decimal(10,2) NOT NULL,
  `valeurReducDemiPart` decimal(10,2) NOT NULL,
  `plafondDecoteCelibataire` decimal(10,2) NOT NULL,
  `plafondDecoteCouple` decimal(10,2) NOT NULL,
  `plafondImpotCelibatairePourDecote` decimal(10,2) NOT NULL,
  `plafondImpotCouplePourDecote` decimal(10,2) NOT NULL,
  `abattementDixPourcentMax` decimal(10,2) NOT NULL,
  `abattementDixPourcentMin` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- --------------------------------------------------------
 
--
-- Table structure for table `tbtranches`
--
 
DROP TABLE IF EXISTS `tbtranches`;
CREATE TABLE `tbtranches` (
  `id` int(11) NOT NULL,
  `limites` decimal(10,2) NOT NULL,
  `coeffR` decimal(10,2) NOT NULL,
  `coeffN` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
--
-- Indexes for dumped tables
--
 
--
-- Indexes for table `tbconstantes`
--
ALTER TABLE `tbconstantes`
  ADD PRIMARY KEY (`id`);
 
--
-- Indexes for table `tbtranches`
--
ALTER TABLE `tbtranches`
  ADD PRIMARY KEY (`id`);
 
--
-- AUTO_INCREMENT for dumped tables
--
 
--
-- AUTO_INCREMENT for table `tbconstantes`
--
ALTER TABLE `tbconstantes`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
 
--
-- AUTO_INCREMENT for table `tbtranches`
--
ALTER TABLE `tbtranches`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
 
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

Pode utilizar este ficheiro SQL para regenerar a base de dados [dbimpots-2019] caso esta tenha sido destruída ou corrompida. Não é necessário eliminar a base de dados antes de a regenerar, uma vez que o script SQL trata disso automaticamente:

Image

Image

13.2. Organização do código

Para ilustrar melhor o papel dos vários scripts PHP que estamos a escrever, vamos organizar o nosso código em pastas:

Image

  • em [1], visão geral da versão 05;
  • em [2], as entidades de aplicação, entidades trocadas entre camadas;
  • em [3], os utilitários da aplicação;
  • em [4], os dados utilizados ou produzidos pela aplicação. Aqui, decidimos utilizar apenas ficheiros JSON para ficheiros de texto. Estes oferecem várias vantagens:
    • são reconhecidos por muitas ferramentas;
    • estas ferramentas suportam o realce de sintaxe. Além disso, o JSON tem regras rigorosas. Quando estas regras não são seguidas, as ferramentas sinalizam-nas. Por exemplo, um erro comum num ficheiro de texto básico é utilizar O maiúsculo ou minúsculo em vez de zeros. Se este erro ocorrer, será sinalizado. No código JSON:

"plafondRevenusCouplePourReduction": 42O74

onde um O maiúsculo foi inadvertidamente utilizado em vez de um zero em [42074], o NetBeans assinala o erro:

Image

Na verdade, o NetBeans reconhece o O maiúsculo, o que torna [49O74] uma cadeia de caracteres. Conclui que a sintaxe deve ser [4-5]: a cadeia de caracteres [47O74] deve ser colocada entre aspas. A atenção do programador é assim chamada para o erro e este pode corrigi-lo: quer adicionando aspas, quer substituindo o O por um zero;

Os outros elementos da versão 05 são os seguintes:

Image

  • em [6], as interfaces e classes da camada [Dao];
  • em [7], as interfaces e classes da camada [business];
  • em [8], os principais scripts da versão 05;

A versão 05 tem dois objetivos distintos:

  • preencher a base de dados MySQL [dbimpots-2019] com o conteúdo do ficheiro JSON [Data/txadmindata.json];
  • implementar o cálculo de impostos utilizando dados fiscais agora obtidos da base de dados MySQL [dbimpots-2019];

Abordaremos estes dois objetivos separadamente.

13.3. Preenchimento da base de dados [dbimpots-2019]

13.3.1. Objetivo

O ficheiro de texto taxadmindata.json contém dados da autoridade fiscal:


{
    "limites": [
        9964,
        27519,
        73779,
        156244,
        0
    ],
    "coeffR": [
        0,
        0.14,
        0.3,
        0.41,
        0.45
    ],
    "coeffN": [
        0,
        1394.96,
        5798,
        13913.69,
        20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
}

O nosso objetivo é transferir estes dados para a base de dados MySQL [dbimpots-2019] criada anteriormente.

13.3.2. As entidades

Image

A entidade [Database] será utilizada para encapsular os dados do seguinte ficheiro JSON [database.json]:


{
    "dsn": "mysql:host=localhost;dbname=dbimpots-2019",
    "id": "admimpots",
    "pwd": "mdpimpots",
    "tableTranches": "tbtranches",
    "colLimites": "limites",
    "colCoeffR": "coeffr",
    "colCoeffN": "coeffn",
    "tableConstantes": "tbconstantes",
    "colPlafondQfDemiPart": "plafondQfDemiPart",
    "colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
    "colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
    "colValeurReducDemiPart": "valeurReducDemiPart",
    "colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
    "colPlafondDecoteCouple": "plafondDecoteCouple",
    "colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
    "colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
    "colAbattementDixPourcentMax": "abattementDixPourcentMax",
    "colAbattementDixPourcentMin": "abattementDixPourcentMin"
}

A entidade [TaxAdminData] será utilizada para encapsular os dados do seguinte ficheiro JSON [taxadmindata.json]:


{
    "limites": [
        9964,
        27519,
        73779,
        156244,
        0
    ],
    "coeffR": [
        0,
        0.14,
        0.3,
        0.41,
        0.45
    ],
    "coeffN": [
        0,
        1394.96,
        5798,
        13913.69,
        20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
}

A entidade [TaxPayerData] será utilizada para encapsular os dados do seguinte ficheiro JSON [taxpayerdata.json]:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

13.3.2.1. A classe base [BaseEntity]

Para simplificar o código da entidade, adotaremos a seguinte regra: os atributos de uma entidade têm os mesmos nomes que os atributos no ficheiro JSON que a entidade se destina a encapsular. Com base nesta regra, as entidades [Database, TaxAdminData, TaxPayerData] partilham características comuns que podem ser agrupadas numa classe pai. Esta será a seguinte classe [BaseEntity]:


<?php
 
namespace Application;
 
class BaseEntity {
  // attribute
  protected $arrayOfAttributes;
 
  // initialization from a jSON file
  public function setFromJsonFile(string $jsonFilename) {
    // retrieve the contents of the tax data file
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // mistake?
    if (!$fileContents) {
      // we note the error
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // retrieve the jSON code from the configuration file in an associative array
      $this->arrayOfAttributes = \json_decode($fileContents, true);
      // mistake?
      if ($this->arrayOfAttributes === FALSE) {
        // we note the error
        $erreur = TRUE;
        $message = "Le fichier de données jSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // mistake?
    if ($erreur) {
      // throw an exception
      throw new ExceptionImpots($message);
    }
    // initialization of class attributes
    foreach ($this->arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // we return the object
    return $this;
  }
 
  public function checkForAllAttributes() {
    // check that all keys have been initialized
    foreach (\array_keys($this->arrayOfAttributes) as $key) {
      if ($key !== "arrayOfAttributes" && !isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de la classe "
          . get_class($this) . " n'a pas été initialisé");
      }
    }
  }
 
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // initialize certain attributes of the
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // object is returned
    return $this;
  }
 
  // toString
  public function __toString() {
    // object attributes
    $arrayOfAttributes = \get_object_vars($this);
    // remove parent class attribute
    unset($arrayOfAttributes["arrayOfAttributes"]);
    // object's Json string
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }
 
  // getter
  public function getArrayOfAttributes() {
    return $this->arrayOfAttributes;
  }
 
}

Comentários

  • linha 5: a classe [BaseEntity] destina-se a ser estendida pelas classes [Database, TaxAdminData, TaxPayerData];
  • linha 7: o atributo [$arrayOfAttributes] é uma matriz que contém todos os atributos da classe filha que estende [BaseEntity], juntamente com os seus valores;
  • linhas 9–41: o atributo [$arrayOfAttributes] é inicializado a partir do ficheiro JSON [$jsonFilename] passado como parâmetro. É lançada uma exceção [ExceptionImpot] se o ficheiro JSON não puder ser lido ou se não for um ficheiro JSON válido;
  • linhas 36–38: este é um código especial se executado por uma classe filha. Neste caso, [$this] representa uma instância da classe filha [Database, TaxAdminData, TaxPayerData] e, neste cenário, as linhas 36–38 inicializam os atributos desta classe filha, desde que esses atributos tenham visibilidade protegida (ou pública) (ver secção em link). Observámos que os atributos das entidades [Database, TaxAdminData, TaxPayerData] são os mesmos que os atributos do ficheiro JSON que encapsulam. Por fim, o método [setFromJsonFile] permite que uma classe filha se inicialize a partir de um ficheiro JSON;
  • linha 40: o objeto [$this] é definido como uma instância da classe filha se o método [setFromJsonFile] tiver sido chamado por uma classe filha;
  • linhas 43–51: o método [checkForAllAttributes] permite que uma classe filha verifique se todos os seus atributos foram inicializados. Se não for esse o caso, é lançada uma exceção [ExceptionImpots]. Este método permite que a classe filha verifique se o seu ficheiro JSON não omitiu determinados atributos;
  • linhas 53–60: O método [setFromArrayOfAttributes] permite que uma classe filha inicialize todos ou alguns dos seus atributos a partir de um array associativo cujas chaves têm os mesmos nomes que os atributos da classe filha a ser inicializada;
  • linhas 63–70: O método [__toString] fornece a representação JSON de uma classe filha;

13.3.2.2. A entidade [Database]

A entidade [Database] é a seguinte:


<?php
 
namespace Application;
 
class Database extends BaseEntity {
  // attributes
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;
 

 
}

A classe [Database] é utilizada para encapsular os dados do seguinte ficheiro JSON [database.json]:


{
    "dsn": "mysql:host=localhost;dbname=dbimpots-2019",
    "id": "admimpots",
    "pwd": "mdpimpots",
    "tableTranches": "tbtranches",
    "colLimites": "limites",
    "colCoeffR": "coeffr",
    "colCoeffN": "coeffn",
    "tableConstantes": "tbconstantes",
    "colPlafondQfDemiPart": "plafondQfDemiPart",
    "colPlafondRevenusCelibatairePourReduction": "plafondRevenusCelibatairePourReduction",
    "colPlafondRevenusCouplePourReduction": "plafondRevenusCouplePourReduction",
    "colValeurReducDemiPart": "valeurReducDemiPart",
    "colPlafondDecoteCelibataire": "plafondDecoteCelibataire",
    "colPlafondDecoteCouple": "plafondDecoteCouple",
    "colPlafondImpotCelibatairePourDecote": "plafondImpotCelibatairePourDecote",
    "colPlafondImpotCouplePourDecote": "plafondImpotCouplePourDecote",
    "colAbattementDixPourcentMax": "abattementDixPourcentMax",
    "colAbattementDixPourcentMin": "abattementDixPourcentMin"
}

A classe e o ficheiro JSON têm os mesmos atributos. Estes descrevem as características da base de dados MySQL [dbimpots-2019]:

dsn
Nome DSN da base de dados
id
Proprietário da base de dados
pwd
A sua palavra-passe
tableTranches
Nome da tabela que contém as faixas de imposto
colLimits
colRate
colCoeffN
Nomes das colunas na tabela [tableTranches]
tableConstants
Nome da tabela que contém as constantes de cálculo de impostos
colIncomeCeilingForHalfShare
colIncomeLimitSingleForReduction
colIncomeLimitCoupleForReduction
colValorDaReduçãoDaMetadeDaQuota
colLimiteCréditoFiscalIndividual
colLimiteMáximoDaDeduçãoParaCasais
colLimiteMáximoDeRendimentoParaIndemnizaçãoIndividual
colLimiteFiscalCasalParaDedução
colDeduçãoMáximaDe10PorCento
colDeduçãoMínimaDe10PorCento
Nomes das colunas na tabela [tableConstants] que contém constantes de cálculo de impostos

Porquê nomear as tabelas e colunas quando já sabemos os seus nomes e é improvável que mudem? Após o SGBD MySQL, utilizaremos o SGBD PostgreSQL para armazenar dados de administração fiscal. No entanto, os nomes das colunas e tabelas do Postgres não seguem as mesmas regras do MySQL. Seremos obrigados a utilizar nomes diferentes. Isto também se aplica a outros SGBDs. Se quisermos código que seja portátil entre SGBDs, é preferível utilizar parâmetros em vez de nomes de tabelas e colunas codificados de forma rígida.

Voltemos ao código da classe [Database]:


<?php
 
namespace Application;
 
class Database extends BaseEntity {
  // attributes
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;
 
  // setter
  // initialization
  public function setFromJsonFile(string $jsonFilename) {
    // parent
    parent::setFromJsonFile($jsonFilename);
    // check that all attributes have been initialized
    parent::checkForAllAttributes();
    // object is returned
    return $this;
  }
 
  // getters and setters
  public function getDsn() {
    return $this->dsn;
  }
 

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

 
}

Comentários

  • linhas 7–24: todos os atributos da classe têm visibilidade [protected]. Este é um requisito para que possam ser modificados a partir da classe pai [BaseEntity] (ver secção indicada);
  • linhas 28–35: o método [setFromJsonFile] permite que os atributos da classe [Database] sejam inicializados a partir do conteúdo de um ficheiro JSON passado como parâmetro. Os atributos no ficheiro JSON e os da classe [Database] devem ser idênticos. Se o ficheiro JSON for inutilizável, é lançada uma exceção;
  • linha 30: a classe pai realiza a inicialização;
  • linha 32: solicita-se à classe pai que verifique se todos os atributos da classe [Database] foram inicializados. Se não for esse o caso, é lançada uma exceção;
  • linha 34: a instância [Database] que acabou de ser inicializada é devolvida;
  • linhas 37 e seguintes: os getters e setters para os atributos da classe;

13.3.2.3. A entidade [TaxAdminData]

A entidade [TaxAdminData] é a seguinte:


<?php
 
namespace Application;
 
class TaxAdminData extends BaseEntity {
  // tax brackets
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // tax calculation constants
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;
 
  
}

A classe [TaxAdminData] é utilizada para encapsular os dados do seguinte ficheiro JSON [taxadmindata.json]:


{
    "limites": [
        9964,
        27519,
        73779,
        156244,
        0
    ],
    "coeffR": [
        0,
        0.14,
        0.3,
        0.41,
        0.45
    ],
    "coeffN": [
        0,
        1394.96,
        5798,
        13913.69,
        20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
}

A classe e o ficheiro JSON têm os mesmos atributos. Estes representam os dados da administração fiscal. O resto do código para a classe [TaxAdminData] é o seguinte:


<?php
 
namespace Application;
 
class TaxAdminData extends BaseEntity {
  // tax brackets
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // tax calculation constants
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;
 
  // initialization
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // parent
    parent::setFromJsonFile($taxAdminDataFilename);
    // check that all attributes have been initialized
    parent::checkForAllAttributes();
    // check that attribute values are real >=0
    foreach ($this as $key => $value) {
      if ($key !== "arrayOfAttributes") {
        // $value must be a real number >=0 or an array of reals >=0
        $result = $this->check($value);
        // mistake?
        if ($result->erreur) {
          // throw an exception
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // we note the value
          $this->$key = $result->value;
        }
      }
    }
    // we return the object
    return $this;
  }
 
  protected function check($value): \stdClass {
    // $value is an array of string elements or a single element
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // transform the array of strings into an array of reals
    $newTableau = [];
    $result = new \stdClass();
    // table elements must be positive or zero decimal numbers
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // put the float in newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // we note the error
        $result->erreur = TRUE;
        // we leave
        return $result;
      }
    }
    // we return the result
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // a single value
      $result->value = $newTableau[0];
    } else {
      // a list of values
      $result->value = $newTableau;
    }
    return $result;
  }
 
  // getters and setters

}

Comentários

  • Linha 23: O método [setFromJsonFile] é utilizado para inicializar os atributos da classe [TaxAdminData] a partir de um ficheiro JSON passado como parâmetro. Os atributos no ficheiro JSON devem ter os mesmos nomes que os da classe;
  • Linha 25: A classe pai executa esta tarefa;
  • linha 27: solicita-se à classe pai que verifique se todos os atributos da classe filha foram inicializados;
  • linhas 29–42: verificamos localmente se todos os atributos têm um valor real positivo ou se são nulos. Esta verificação já foi discutida na secção «link» da versão 03;

13.3.3. A camada [dao]

Agora podemos escrever o código que irá transferir dados do ficheiro de texto [taxadmindata.json] para as tabelas [tbtranches, tbconstantes] da base de dados MySQL [dbimpots-2019]. Iremos adotar a seguinte arquitetura:

Image

Image

A camada [dao] irá implementar a seguinte interface [InterfaceDao4TransferAdminDataFromFile2Database]:


<?php
 
// namespace
namespace Application;
 
interface InterfaceDao4TransferAdminData2Database {
 
  public function transferAdminData2Database(): void;
}

Comentários

  • linha 8: o método [transferAdminData2Database] é responsável por armazenar dados de administração fiscal numa base de dados;

A interface [InterfaceDao4TransferAdminData2Database] será implementada pela seguinte classe [DaoTransferAdminDataFromJsonFile2Database]:


<?php
 
// namespace
namespace Application;
 
// definition of a TransferAdminDataFromFile2DatabaseDao class
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database {
  // target database attributes
  private $database;
  // tax administration data
  private $taxAdminData;
 
  // manufacturer
  public function __construct(string $databaseFilename, string $taxAdminDataFilename) {
    // save configuration
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // tax data is stored
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }
 
  // transfers tax bracket data from a text file
  // to database
  public function transferAdminData2Database(): void {
    // we work on the basis
    $database = $this->database;
    try {
      // open the database connection
      $connexion = new \PDO($database->getDsn(), $database->getId(), $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->fillTableTranches($connexion);
      // fill in the constants table
      $this->fillTableConstantes($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;
    }
  }
 
 
  // filling the tax bracket table
  private function fillTableTranches($connexion): void {

  }
 
  // filling the constants table
  private function fillTableConstantes($connexion): void {

  }
 
}

Comentários

Aqui estamos a aplicar o que aprendemos no capítulo sobre o MySQL.

  • linha 7: a classe [DaoTransferAdminDataFromJsonFile2Database] implementa a interface [InterfaceDao4TransferAdminData2Database];
  • linha 9: o atributo [$database] é o objeto do tipo [Database] que encapsula os dados do ficheiro [database.json];
  • linha 11: o atributo [$taxAdminData] é um objeto do tipo [TaxAdminData] que encapsula os dados do ficheiro [taxadmindata.json];
  • linhas 14–19: o construtor recebe os nomes dos ficheiros [database.json, taxadmindata.json] como parâmetros;
  • linha 16: inicialização do atributo [$database];
  • linha 18: inicialização do atributo [$taxAdminData];
  • linha 23: o único método da interface [InterfaceDao4TransferAdminData2Database] é implementado;
  • Linhas 26–38: As tabelas [tbtranches] e [tbconstantes] são preenchidas em duas etapas:
    • linha 34: Primeiro, a tabela [tbtranches] é preenchida. Isto é feito dentro de uma transação (linhas 32, 38). O método [fillTableTranches] (linha 55) lança uma exceção assim que algo corre mal. Neste caso, a execução continua com o bloco catch / finally nas linhas 39–50;
    • linha 36: a tabela [tbconstantes] é preenchida da mesma forma utilizando o método [fillTableConstantes] (linha 60);
  • linhas 39–47: caso em que uma exceção foi lançada pelo código;
  • linhas 41–44: se existir uma transação, esta é revertida;
  • linha 46: é lançada uma exceção do tipo [ExceptionImpots] com a mensagem da exceção original, que pode ser de qualquer tipo;
  • linhas 47–50: na cláusula [finally], a ligação é encerrada;

O código do método [fillTableTranches] é o seguinte:


private function fillTableTranches($connexion): void {
    // raccourci pour la bd
    $database = $this->database;
    // les données à insérer dans la base de données
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // on vide la table au cas où il y aurait qq chose dedans
    $statement = $connexion->prepare("delete from " . $database->getTableTranches());
    $statement->execute();
    // on prépare les insertions
    $sqlInsert = "insert into {$database->getTableTranches()} "
      . "({$database->getColLimites()}, {$database->getColCoeffR()},"
      . " {$database->getColCoeffN()}) values (:limites, :coeffR, :coeffN)";
    $statement = $connexion->prepare($sqlInsert);
    // on exécute l'ordre préparé avec les valeurs des tranches d'impôts
    for ($i = 0; $i < count($limites); $i++) {
      $statement->execute([
        "limites" => $limites[$i],
        "coeffR" => $coeffR[$i],
        "coeffN" => $coeffN[$i]]);
    }
  }

Comentários

  • linha 1: o método [fillTableTranches] recebe uma ligação aberta como parâmetro. Sabemos também que uma transação foi iniciada dentro desta ligação;
  • linhas 5–7: os valores a inserir na tabela são fornecidos pelo atributo [$taxAdminData];
  • linhas 9–10: o conteúdo atual da tabela [tbtranches] é limpo;
  • linhas 12–15: Preparamo-nos para inserir linhas na tabela. Aqui, utilizamos os nomes das colunas fornecidos pelo atributo [$database];
  • linhas 17–22: a instrução de inserção preparada nas linhas 12–15 é executada tantas vezes quantas forem necessárias;

O código para o método [fillTableConstantes] é o seguinte:


private function fillTableConstantes($connexion): void {
    // raccourci
    $database = $this->database;
    // on vide la table au cas où il y aurait qq chose dedans
    $statement = $connexion->prepare("delete from {$database->getTableConstantes()}");
    $statement->execute();
    // on prépare l'insertion
    $taxAdminData = $this->taxAdminData;
    $sqlInsert = "insert into {$database->getTableConstantes()}"
      . " ({$database->getColPlafondQfDemiPart()},"
      . " {$database->getColPlafondRevenusCelibatairePourReduction()},"
      . " {$database->getColPlafondRevenusCouplePourReduction()},"
      . " {$database->getColValeurReducDemiPart()},"
      . " {$database->getColPlafondDecoteCelibataire()},"
      . " {$database->getColPlafondDecoteCouple()},"
      . " {$database->getColPlafondImpotCelibatairePourDecote()},"
      . " {$database->getColPlafondImpotCouplePourDecote()},"
      . " {$database->getColAbattementDixPourcentMax()},"
      . " {$database->getColAbattementDixPourcentMin()})"
      . " values ("
      . ":plafondQfDemiPart,"
      . ":plafondRevenusCelibatairePourReduction,"
      . ":plafondRevenusCouplePourReduction,"
      . ":valeurReducDemiPart,"
      . ":plafondDecoteCelibataire,"
      . ":plafondDecoteCouple,"
      . ":plafondImpotCelibatairePourDecote,"
      . ":plafondImpotCouplePourDecote,"
      . ":abattementDixPourcentMax,"
      . ":abattementDixPourcentMin)";
    $statement = $connexion->prepare($sqlInsert);
    // on exécute l'ordre préparé
    $statement->execute([
      "plafondQfDemiPart" => $taxAdminData->getPlafondQfDemiPart(),
      "plafondRevenusCelibatairePourReduction" => $taxAdminData->getPlafondRevenusCelibatairePourReduction(),
      "plafondRevenusCouplePourReduction" => $taxAdminData->getPlafondRevenusCouplePourReduction(),
      "valeurReducDemiPart" => $taxAdminData->getValeurReducDemiPart(),
      "plafondDecoteCelibataire" => $taxAdminData->getPlafondDecoteCelibataire(),
      "plafondDecoteCouple" => $taxAdminData->getPlafondDecoteCouple(),
      "plafondImpotCelibatairePourDecote" => $taxAdminData->getPlafondImpotCelibatairePourDecote(),
      "plafondImpotCouplePourDecote" => $taxAdminData->getPlafondImpotCouplePourDecote(),
      "abattementDixPourcentMax" => $taxAdminData->getAbattementDixPourcentMax(),
      "abattementDixPourcentMin" => $taxAdminData->getAbattementDixPourcentMin()
    ]);
  }

Comentários

  • linha 1: o método [fillTableConstantes] recebe uma ligação aberta como parâmetro. Sabemos também que foi iniciada uma transação dentro desta ligação;
  • linhas 5-6: a tabela [tbconstantes] é esvaziada;
  • linhas 9–31: preparação da instrução SQL INSERT. É complexo porque há 10 colunas a inicializar nesta operação de inserção e os nomes das colunas têm de ser recuperados do atributo [$database];
  • linhas 33–44: execução da instrução insert. Há apenas uma linha para inserir. Mais uma vez, o código é complexo devido à necessidade de recuperar os valores a inserir a partir do atributo [$taxAdminData];

13.3.4. O script principal

Image

Image

O script principal depende da camada [dao] para realizar a transferência de dados:


<?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__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/InterfaceDao4TransferAdminData2Database.php";
require_once __DIR__ . "/../Dao/DaoTransferAdminDataFromJsonFile2Database.php";
//
// definition of constants
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
 
//
try {
  // creation of the [dao] layer
  $dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
  // data transfer to the database
  $dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
  // error is displayed
  print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// end
print "Terminé\n";
exit;

Comentários

  • linhas 12–21: carregamento das classes e interfaces da aplicação;
  • linhas 24–24: os dois ficheiros JSON;
  • linha 30: instanciar a camada [DAO] passando os dois ficheiros JSON para o construtor;
  • linha 32: a transferência de dados é realizada;

Quando executamos este código, obtemos o seguinte resultado na base de dados:

Image

A coluna [3] mostra os valores atribuídos pelo MySQL à chave primária [id]. A numeração começa em 1. A captura de ecrã acima foi tirada após a execução do script várias vezes.

Image

Image

13.4. Cálculo de impostos

Image

13.4.1. Arquitetura

A versão 04 da aplicação de cálculo de impostos utilizava uma arquitetura em camadas:

Image

A camada [dao] implementa uma interface [InterfaceDao]. Criámos uma classe que implementa esta interface:

  • [DaoImpotsWithTaxAdminDataInJsonFile] que recuperava dados fiscais de um ficheiro JSON. Essa era a versão 04;

Vamos implementar a interface [InterfaceDao] utilizando uma nova classe [DaoImpotsWithTaxAdminDataInDatabase] que irá recuperar dados da administração fiscal de uma base de dados MySQL. A camada [dao], tal como antes, irá gravar resultados e erros em ficheiros de texto e recuperar dados dos contribuintes a partir de um ficheiro de texto também. Só que, desta vez, esses ficheiros de texto serão ficheiros JSON. Além disso, sabemos que, se continuarmos a aderir à interface [InterfaceDao], a camada [business] não precisará de ser modificada.

Image

13.4.2. A entidade [TaxPayerData]

Image

A classe [TaxPayerData] é utilizada para encapsular os dados do seguinte ficheiro JSON [taxpayersdata.json] numa classe:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

A classe [TaxPayerData] é a seguinte:


<?php
 
// namespace
namespace Application;
 
// data class
class TaxPayerData extends BaseEntity {
  // data required to calculate the taxpayer's tax liability
  protected $marié;
  protected $enfants;
  protected $salaire;
  // tax calculation results
  protected $impôt;
  protected $surcôte;
  protected $décôte;
  protected $réduction;
  protected $taux;
 
  // getters and setters

}

Comentários

  • linha 7: a classe [TaxPayerData] estende a classe [BaseEntity]. Uma vez que os métodos da sua classe pai são suficientes, a classe [TaxPayerData] não define nenhum método próprio. Note-se que os atributos da classe [TaxPayerData] são idênticos aos do ficheiro JSON [taxpayersdata.json];

13.4.3. A camada [dao]

13.4.3.1. A característica [TraitDao]

A característica [TraitDao] implementa parte da interface [InterfaceDao]. Vamos revê-la:


<?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;
}
 

A característica [TraitDao] implementa os métodos [getTaxPayersData] e [saveResults] da interface [InterfaceDao]. Uma vez que a definição da entidade [TaxPayerData] foi alterada entre as versões 04 e 05, precisamos de atualizar o código em [TraitDao]:


<?php
 
// namespace
namespace Application;
 
trait TraitDao {
 
  // reading taxpayer data
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // retrieve taxpayer data in a table
    $baseEntity = new BaseEntity();
    $baseEntity->setFromJsonFile($taxPayersFilename);
    $arrayOfAttributes = $baseEntity->getArrayOfAttributes();
    // taxpayer data table
    $taxPayersData = [];
    // error table
    $errors = [];
    // we loop over the array of attributes of elements of type [TaxPayerData]
    $i = 0;
    foreach ($arrayOfAttributes as $attributesOfTaxPayerData) {
      // check
      $error = $this->check($attributesOfTaxPayerData);
      if (!$error) {
        // one more taxpayer
        $taxPayersData[] = (new TaxPayerData())->setFrOmArrayOfAttributes($attributesOfTaxPayerData);
      } else {
        // an error of + - the invalid data number is noted
        $error = ["numéro" => $i] + $error;
        $errors[] = $error;
      }
      // following
      $i++;
    }
    // save errors in a json file
    $string = "";
    foreach ($errors as $error) {
      $string .= \json_encode($error, JSON_UNESCAPED_UNICODE) . "\n";
    }
    $this->saveString($errorsFilename, $string);
    // function result
    return $taxPayersData;
  }
 
  private function check(array $attributesOfTaxPayerData): array {
    // check the data in [$taxPayerData]
    // the list of erroneous attributes
    $attributes = [];
    // marital status must be yes or no
    $marié = trim(strtolower($attributesOfTaxPayerData["marié"]));
    $erreur = ($marié !== "oui" and $marié !== "non");
    if ($erreur) {
      // we note the error
      $attributes[] = ["marié" => $marié];
    }
    // the number of children must be a positive integer or zero
    $enfants = trim($attributesOfTaxPayerData["enfants"]);
    if (!preg_match("/^\d+$/", $enfants)) {
      // we note the error
      $erreur = TRUE;
      $attributes[] = ["enfants" => $enfants];
    } else {
      $enfants = (int) $enfants;
    }
 
    // the salary must be a positive integer or zero (without euro cents)
    $salaire = trim($attributesOfTaxPayerData["salaire"]);
    if (!preg_match("/^\d+$/", $salaire)) {
      // we note the error
      $erreur = TRUE;
      $attributes[] = ["salaire" => $salaire];
    } else {
      $salaire = (int) $salaire;
    }
 
    // mistake?
    if ($erreur) {
      // return with error
      return ["erreurs" => $attributes];
    } else {
      // error-free return
      return [];
    }
  }
 
  // 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
    // construction of the jSON results chain
    $string = "[" . implode(",
", $taxPayersData) . "]";
    // recording this channel
    $this->saveString($resultsFilename, $string);
  }
 
  // saving table results in a text file
  private function saveString(string $fileName, string $data): void {
    // save string [$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

  • [TraitDao] implementa os métodos [getTaxPayersData] (linha 9) e [saveResults] (linha 86) da interface [InterfaceDao];
  • linha 9: o método [getTaxPayersData] aceita os seguintes parâmetros:
    • [$taxPayersFilename]: o nome do ficheiro JSON que contém os dados dos contribuintes [taxpayersdata.json];
    • [$errorsFilename]: o nome do ficheiro JSON que contém os erros [errors.json];
  • linhas 11–13: o conteúdo do ficheiro JSON contendo os dados dos contribuintes é transferido para uma matriz associativa [$arrayOfAttributes]. Se o ficheiro JSON se revelar inutilizável, é lançada uma exceção [ExceptionImpots];
  • linha 15: a matriz [$taxPayersData] conterá os dados dos contribuintes encapsulados em objetos do tipo [TaxPayerData];
  • linha 17: os erros são acumulados na matriz [$errors];
  • linhas 99–33: construção da matriz [$taxPayersData];
  • linha 22: Antes de serem encapsulados no tipo [TaxPayerData], os dados são verificados. O método [check] devolve:
    • um array [‘errors’=>[…]] contendo os atributos errados, se os dados estiverem incorretos;
    • um array vazio se os dados estiverem corretos;
  • Linha 25: Quando os dados são válidos. É criado um novo objeto [TaxPayerData] e adicionado à matriz [$taxPayersData];
  • linhas 26–30: caso em que os dados sejam inválidos. O registo de erro inclui o ID do objeto [TaxPayerData] incorreto no ficheiro JSON para que o utilizador o possa localizar; em seguida, o erro é adicionado à matriz [$errors];
  • linhas 35–39: os erros encontrados são registados no ficheiro JSON [$errorsFilename] passado como parâmetro na linha 9;
  • linha 41: a matriz de objetos [TaxPayerData] construídos é devolvida: este era o objetivo do método;
  • linhas 44–83: o método privado [check] verifica a validade dos parâmetros [married, children, salary] da matriz [$attributesOfTaxPayerData] passada como parâmetro na linha 44. Se houver algum atributo inválido, este é recolhido na matriz [$attributes] (linhas 47, 53, 60, 70) na forma de uma matriz [‘invalid attribute’=> valor do atributo inválido];
  • linha 78: se houver erros, retorna uma matriz [‘errors’=>$attributes];
  • linha 81: se não houver erros, é devolvida uma matriz vazia de erros;
  • linhas 86–93: implementação do método [saveResults] da interface [InterfaceDao];
  • linha 90: construímos a cadeia JSON a ser guardada no ficheiro JSON [$resultsFilename] passado como parâmetro na linha 86. Temos de construir a cadeia JSON a partir de uma matriz:
    • cada elemento da matriz é separado do seguinte por uma vírgula e uma nova linha;
    • a matriz inteira é colocada entre colchetes [];
  • linha 92: a cadeia JSON é guardada no ficheiro JSON [$resultsFilename];

13.4.3.2. A classe [DaoImpotsWithTaxAdminDataInDatabase]

A classe [DaoImpotsWithTaxAdminDataInDatabase] implementa a interface [InterfaceDao] da seguinte forma:


<?php
 
// namespace
namespace Application;
 
// definition of a ImpotsWithDataInDatabase class
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao {
  // use of a line
  use TraitDao;
  // 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;
  }
 
}
 

Comentários

  • linha 4: mantemos o namespace já utilizado nas outras implementações da camada [dao];
  • linha 7: a classe [DaoImportsWithTaxAdminDataInDatabase] implementa a interface [InterfaceDao];
  • linha 9: importamos o trait [TraitDao]. Sabemos que este trait implementa parte da interface. O único método que resta implementar é o método [getTaxAdminData] nas linhas 62–64. Este método simplesmente devolve o atributo privado [taxAdminData] da linha 11. Podemos inferir que o construtor deve inicializar este atributo. Essa é a sua única função;
  • Linha 16: O construtor recebe um único parâmetro, [$databaseFilename], que é o nome do ficheiro JSON [database.json] que define a base de dados MySQL [dbimpots-2019];
  • linha 18: o ficheiro JSON [$databaseFilename] é utilizado para criar um objeto [Database], que é construído e armazenado no atributo [$database] da linha 13. Se o ficheiro JSON não puder ser processado corretamente, é lançada uma exceção [ExceptionImpots];
  • linha 20: o objeto [$this→taxAdminData] é criado, o qual o construtor deve inicializar;
  • linhas 22–26: a ligação à base de dados é aberta. Repare na notação [\PDO] para se referir à classe [PDO] do PHP. Uma vez que estamos no namespace [Application], se escrevêssemos simplesmente [PDO], este nome relativo seria prefixado com o namespace atual, resultando na classe [Application\PDO], que não existe;
  • linha 28: se ocorrer um erro, o SGBD lançará uma \PDOException (linha 37);
  • linha 30: iniciamos uma transação. Isto não é realmente necessário, uma vez que apenas duas instruções SQL serão executadas e estas instruções não modificam a base de dados. Fazemos isto, no entanto, para nos isolarmos de outros utilizadores da base de dados;
  • linha 32: a tabela de faixas de imposto [tbtranches] é lida utilizando o método privado [getTranches] da linha 52;
  • linha 34: a tabela de constantes de cálculo [tbconstantes] é lida utilizando o método privado [getConstantes] da linha 57;
  • linha 36: se chegarmos a esta linha, significa que tudo correu bem. Por isso, confirmamos a transação;
  • linhas 37–42: se chegarmos a este ponto, significa que ocorreu uma exceção. Por isso, revertemos a transação, caso houvesse uma em curso (linhas 39–42). Linha 44: para garantir a consistência das exceções, re-lançamos a mensagem de exceção recebida, desta vez como uma exceção do tipo [ExceptionImpots];
  • linhas 45–48: Em todos os casos (quer tenha ocorrido uma exceção ou não), encerramos a ligação;

O método [getTranches] é o seguinte:


private function getTranches($connexion): void {
    // raccourcis
    $database = $this->database;
    $taxAdminData = $this->taxAdminData;
    // on prépare la requête SELECT
    $statement = $connexion->prepare(
      "select {$database->getColLimites()}," .
      " {$database->getColCoeffR()}," .
      " {$database->getColCoeffN()}" .
      " from {$database->getTableTranches()}");
    // on exécute l'ordre préparé avec les valeurs des tranches d'impôts
    $statement->execute();
    // on exploite le résultat
    $limites = [];
    $coeffR = [];
    $coeffN = [];
    // remplissage des trois tableaux
    while ($tranche = $statement->fetch(\PDO::FETCH_OBJ)) {
      $limites[] = (float) $tranche->{$database->getColLimites()};
      $coeffR[] = (float) $tranche->{$database->getColCoeffR()};
      $coeffN[] = (float) $tranche->{$database->getColCoeffN()};
    }
    // on mémorise les données dans l'attribut [$taxAdminData] de la classe
    $taxAdminData->setFromArrayOfAttributes([
      "limites" => $limites,
      "coeffR" => $coeffR,
      "coeffN" => $coeffN
    ]);
  }

Comentários

  • linha 1: o método recebe [$connexion] como parâmetro, que é uma ligação aberta com uma transação em curso;
  • linhas 2–4: são criados dois atalhos para evitar ter de escrever [$this->database] e [$taxAdminData = $this->taxAdminData] ao longo do código. Trata-se de cópias de referências a objetos, não de cópias dos próprios objetos;
  • linhas 6–10: a instrução SELECT é preparada e, em seguida, executada na linha 12;
  • linhas 13–22: o resultado da instrução SELECT é processado. A informação recebida é armazenada em três matrizes [limits, coeffR, coeffN];
  • linhas 24–28: as três matrizes são utilizadas para inicializar o atributo [$this->taxAdminData] da classe;

O método privado [getConstantes] é o seguinte:


private function getConstantes($connexion): void {
    // raccourcis
    $database = $this->database;
    $taxAdminData = $this->taxAdminData;
    // on prépare la requête SELECT
    $select = "select {$database->getColPlafondQfDemiPart()}," .
      "{$database->getColPlafondRevenusCelibatairePourReduction()}," .
      "{$database->getColPlafondRevenusCouplePourReduction()}," . "{$database->getColValeurReducDemiPart()}," .
      "{$database->getColPlafondDecoteCelibataire()}," . "{$database->getColPlafondDecoteCouple()}," .
      "{$database->getColPlafondImpotCelibatairePourDecote()}," . "{$database->getColPlafondImpotCouplePourDecote()}," .
      "{$database->getColAbattementDixPourcentMax()}," . "{$database->getColAbattementDixPourcentMin()}" .
      " from {$database->getTableConstantes()}";
    $statement = $connexion->prepare($select);
    // on exécute l'ordre préparé
    $statement->execute();
    // on exploite le résultat - 1 seule ligne ici
    $row = $statement->fetch(\PDO::FETCH_OBJ);
    // on initialise l'attribut [$taxAdminData]
    $taxAdminData->setPlafondQfDemiPart($row->{$database->getColPlafondQfDemiPart()});
    $taxAdminData->setPlafondRevenusCelibatairePourReduction(
      $row->{$database->getColPlafondRevenusCelibatairePourReduction()});
    $taxAdminData->setPlafondRevenusCouplePourReduction($row->{$database->getColPlafondRevenusCouplePourReduction()});
    $taxAdminData->setValeurReducDemiPart($row->{$database->getColValeurReducDemiPart()});
    $taxAdminData->setPlafondDecoteCelibataire($row->{$database->getColPlafondDecoteCelibataire()});
    $taxAdminData->setPlafondDecoteCouple($row->{$database->getColPlafondDecoteCouple()});
    $taxAdminData->setPlafondImpotCelibatairePourDecote($row->{$database->getColPlafondImpotCelibatairePourDecote()});
    $taxAdminData->setPlafondImpotCouplePourDecote($row->{$database->getColPlafondImpotCouplePourDecote()});
    $taxAdminData->setAbattementDixPourcentMax($row->{$database->getColAbattementDixPourcentMax()});
    $taxAdminData->setAbattementDixPourcentMin($row->{$database->getColAbattementDixPourcentMin()});
  }

Comentários

  • linha 1: o método recebe [$connection] como parâmetro, que é uma ligação aberta com uma transação em curso;
  • linhas 2–4: são criados dois atalhos para evitar ter de escrever [$this->database] e [$taxAdminData = $this->taxAdminData] ao longo do código. Estas são cópias de referências a objetos, não cópias dos próprios objetos;
  • linhas 6–15: a instrução SELECT é preparada e, em seguida, executada na linha 15;
  • linhas 17–29: o resultado da instrução SELECT é processado. A informação recuperada é utilizada para inicializar o atributo [$this->taxAdminData] da classe;

Nota: Note que a classe não depende do SGBD MySQL. É o código de chamada que especifica o SGBD utilizado através do DSN da base de dados.

13.4.4. A camada [business]

Image

  • Acabámos de implementar a camada [DAO] (3);
  • Uma vez que aderimos à interface [InterfaceDao], a camada [business] (2) pode, teoricamente, permanecer inalterada. No entanto, não modificámos apenas a camada [DAO]. Também modificámos as entidades, que são partilhadas por todas as camadas;

A camada [business] implementa a seguinte interface [BusinessInterface]:


<?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;
}
  • linha 12: o método [executeBatchImports] utiliza agora o ficheiro JSON [$taxPayersFileName], enquanto que na versão 04 era um ficheiro de texto simples. ;

Na versão 04, o método [executeBatchImpots] era 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);
  }
  • A linha 15 está agora incorreta. Na nova definição da classe [TaxPayerData], o método [setMontant] já não existe;

Na versão 05, o método [executeBatchImpots] será 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->setFromArrayOfAttributes($result);
      // put the result in the results table
      $results [] = $taxPayerData;
    }
    // recording results
    $this->dao->saveResults($resultsFileName, $results);
  }

Comentários

  • linha 15: em vez de utilizar os setters individuais da classe [TaxPayerData], utilizamos o seu setter global [setFromArrayOfAttributes];
  • o resto do código não precisa de ser modificado;

13.4.5. O script principal

Image

  • Acabámos de implementar as camadas [DAO] (3) e [lógica de negócio] (2);
  • ainda precisamos de escrever o script principal (1);

O script principal é semelhante ao da versão 04:


<?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__ . "/../Entities/BaseEntity.php";
require_once __DIR__ . "/../Entities/TaxAdminData.php";
require_once __DIR__ . "/../Entities/TaxPayerData.php";
require_once __DIR__ . "/../Entities/Database.php";
require_once __DIR__ . "/../Entities/ExceptionImpots.php";
require_once __DIR__ . "/../Utilities/Utilitaires.php";
require_once __DIR__ . "/../Dao/InterfaceDao.php";
require_once __DIR__ . "/../Dao/TraitDao.php";
require_once __DIR__ . "/../Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once __DIR__ . "/../Métier/InterfaceMetier.php";
require_once __DIR__ . "/../Métier/Metier.php";
//
// definition of constants
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
const RESULTS_FILENAME = "../Data/resultats.json";
const ERRORS_FILENAME = "../Data/errors.json";
const TAXPAYERSDATA_FILENAME = "../Data/taxpayersdata.json";
 
try {
  // creation of the [dao] layer
  $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_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 "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// end
print "Terminé\n";
exit;
 
 

Comentários

  • linhas 12–22: carregamento de todos os ficheiros da versão 05;
  • linhas 25–29: os nomes dos vários ficheiros JSON da aplicação;
  • linha 33: construção da camada [DAO];
  • linha 35: construção da camada [business];
  • linha 37: chamada do método [executeBatchImports] da camada [business];

Resultados

A aplicação gera dois ficheiros JSON:

  • [results.json]: os resultados dos vários cálculos fiscais;
  • [errors.json]: que relata os erros encontrados no ficheiro JSON [taxpayersdata.json];

O ficheiro [errors.json] é o seguinte:


{
    "numéro": 1,
    "erreurs": [
        {
            "marié": "ouix"
        },
        {
            "enfants": "2x"
        },
        {
            "salaire": "55555x"
        }
    ]
}

Isto significa que, no ficheiro [taxpayersdata.json], a primeira entrada na tabela taxpayers está incorreta. O ficheiro [taxpayersdata.json] tinha o seguinte conteúdo:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555
    },
    {
        "marié": "ouix",
        "enfants": "2x",
        "salaire": "55555x"
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000
    }
]

O ficheiro de resultados [results.json] é o seguinte:


[
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 55555,
        "impôt": 2814,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "marié": "oui",
        "enfants": "2",
        "salaire": 50000,
        "impôt": 1384,
        "surcôte": 0,
        "décôte": 384,
        "réduction": 347,
        "taux": 0.14
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 50000,
        "impôt": 0,
        "surcôte": 0,
        "décôte": 720,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "marié": "non",
        "enfants": 2,
        "salaire": 100000,
        "impôt": 19884,
        "surcôte": 4480,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.41
    },
    {
        "marié": "non",
        "enfants": 3,
        "salaire": 100000,
        "impôt": 16782,
        "surcôte": 7176,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.41
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 100000,
        "impôt": 9200,
        "surcôte": 2180,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.3
    },
    {
        "marié": "oui",
        "enfants": 5,
        "salaire": 100000,
        "impôt": 4230,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.14
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 100000,
        "impôt": 22986,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.41
    },
    {
        "marié": "oui",
        "enfants": 2,
        "salaire": 30000,
        "impôt": 0,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0
    },
    {
        "marié": "non",
        "enfants": 0,
        "salaire": 200000,
        "impôt": 64210,
        "surcôte": 7498,
        "décôte": 0,
        "réduction": 0,
        "taux": 0.45
    },
    {
        "marié": "oui",
        "enfants": 3,
        "salaire": 20000,
        "impôt": 0,
        "surcôte": 0,
        "décôte": 0,
        "réduction": 0,
        "taux": 0
    }
]

Estes resultados são consistentes com os da versão 04.

13.5. Testes [Codeception]

Tal como foi feito na secção ligada para a versão 04, iremos escrever testes [Codeception] para a versão 05.

Image

13.5.1. Testar a camada [dao]

O teste [DaoTest.php] é o seguinte:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// root directories
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
 
// interface and class inclusion
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// third-party libraries
require_once VENDOR . "/autoload.php";
 
// definition of constants
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";
 
class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;
 
  public function __construct() {
    parent::__construct();
    // creation of the [dao] layer
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }
 
  // tests
  public function testTaxAdminData() {
    // calculation constants
    $this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());

  }
 
}

Comentários

  • linhas 9–33: definição do ambiente de teste. Utilizamos o mesmo que o script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] descrito na secção com o link;
  • linhas 39–44: construção da camada [dao];
  • linha 43: o atributo [$this→taxAdminData] contém os dados a testar;
  • linhas 47–51: o método [testTaxAdminData] é o descrito na secção com o link;

Os resultados do teste são os seguintes:

Image

13.5.2. Teste da camada [business]

O teste [MetierTest.php] é o seguinte:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Application;
 
// root directories
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
 
// interface and class inclusion
require_once ROOT . "/Entities/BaseEntity.php";
require_once ROOT . "/Entities/TaxAdminData.php";
require_once ROOT . "/Entities/TaxPayerData.php";
require_once ROOT . "/Entities/Database.php";
require_once ROOT . "/Entities/ExceptionImpots.php";
require_once ROOT . "/Utilities/Utilitaires.php";
require_once ROOT . "/Dao/InterfaceDao.php";
require_once ROOT . "/Dao/TraitDao.php";
require_once ROOT . "/Dao/DaoImpotsWithTaxAdminDataInDatabase.php";
require_once ROOT . "/Métier/InterfaceMetier.php";
require_once ROOT . "/Métier/Metier.php";
// third-party libraries
require_once VENDOR . "/autoload.php";
 
// definition of constants
const DATABASE_CONFIG_FILENAME = ROOT ."/Data/database.json";
const TAXADMINDATA_FILENAME = ROOT ."/Data/taxadmindata.json";
const RESULTS_FILENAME = ROOT ."/Data/resultats.json";
const ERRORS_FILENAME = ROOT ."/Data/errors.json";
const TAXPAYERSDATA_FILENAME = ROOT ."/Data/taxpayersdata.json";
 
class MetierTest extends \Codeception\Test\Unit {
  // business layer
  private $métier;
 
  public function __construct() {
    parent::__construct();
    // creation of the [dao] layer
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_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 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 9–33: definição do ambiente de teste. Utilizamos o mesmo que o script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] descrito na secção com o link;
  • linhas 39–45: construção das camadas [dao] e [business];
  • linha 44: o atributo [$this→business] faz referência à camada [business];
  • linhas 47–64: os métodos [test1, test2…, test11] são os descritos na secção com o link;

Os resultados do teste são os seguintes:

Image