Skip to content

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

Image

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

Image

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

  • [DaoImpotsWithTaxAdminDataInJsonFile], que iria buscar os dados fiscais a um ficheiro jSON;

Vamos implementar a interface [InterfaceDao] através de uma nova classe [DaoImpotsWithTaxAdminDataInDatabase], que irá buscar os dados da administração fiscal numa base de dados MySQL.

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

Seguindo o exemplo do parágrafo com o link, criamos uma base de dados MySQL denominada [dbimpots-2019], cujo proprietário será [admimpots] com a palavra-passe [mdpimpots]:

Image

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

Image

  • no [1-5] acima, vemos que o utilizador [admimpots] tem todos os direitos sobre a 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á os escalões de tributação:

Image

  • em [1-7], criamos uma tabela denominada [tbtranches] com 4 colunas;

Image

  • em [3-6], definimos uma coluna denominada [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 por si próprio os valores da chave primária no momento das inserções. Atribuirá o valor 1 à chave primária da primeira inserção, depois 2 à seguinte, etc.;
  • no [7], o assistente sugere-nos outras opções de configuração da chave primária. Aqui, limitamo-nos a aceitar os valores predefinidos no [7];

Image

  • em [8-16], definem-se as outras três colunas da tabela:
    • [limites] (8), do tipo número decimal (9) com 10 algarismos, dos quais 2 decimais (10), conterá os elementos da coluna 17 das faixas de imposto;
    • [coeffR] (11), do tipo número decimal (12) com 6 algarismos, dos quais 2 decimais (13), conterá os elementos da coluna 18 das faixas de imposto;
    • [coeffN] (14), do tipo número decimal (15) com 10 dígitos, dos quais 2 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 da chave indica que a coluna [id] é a chave primária. Vemos também que esta chave primária tem valores inteiros (6) e que é gerida (autoincrementada) por MySQL;

Da mesma forma que criámos a tabela [tbtranches], criamos a tabela [tbconstantes], que conterá as constantes do cálculo do imposto:

Image

É possível exportar a estrutura da base de dados para um ficheiro de texto sob a forma de uma sequência de comandos SQL:

Image

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

Image

Image

Image

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


-- phpMyAdmin SQL Dump
-- versão 4.8.5
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Hora de geração: 30 de junho de 2019 às 01:10 PM
-- Versão do servidor: 5.7.24
-- PHP Versão: 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 */;

--
-- Base de dados: `dbimpots-2019`
--
CREATE DATABASE IF NOT EXISTS `dbimpots-2019` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `dbimpots-2019`;

-- --------------------------------------------------------

--
-- Estrutura da tabela `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;

-- --------------------------------------------------------

--
-- Estrutura da tabela `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;

--
-- Índices das tabelas exportadas
--

--
-- Índices da tabela `tbconstantes`
--
ALTER TABLE `tbconstantes`
  ADD PRIMARY KEY (`id`);

--
-- Índices da tabela `tbtranches`
--
ALTER TABLE `tbtranches`
  ADD PRIMARY KEY (`id`);

--
-- AUTO_INCREMENT para as tabelas exportadas
--

--
-- AUTO_INCREMENT para a tabela `tbconstantes`
--
ALTER TABLE `tbconstantes`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

--
-- AUTO_INCREMENT para a tabela `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 alterada. Não é necessário eliminar a base de dados antes de a regenerar, uma vez que o script SQL encarrega-se de o fazer:

Image

Image

13.2. Organização do código

Para ilustrar melhor o papel dos diferentes 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 da aplicação, entidades trocadas entre camadas;
  • em [3], os utilitários da aplicação;
  • em [4], os dados utilizados ou produzidos pela aplicação. Decidimos aqui utilizar apenas ficheiros jSON para os ficheiros de texto. Estes apresentam várias vantagens:
    • são reconhecidos por muitas ferramentas;
    • estas ferramentas dispõem de colorização sintática. Além disso, a notação jSON segue regras específicas. Quando estas não são respeitadas, as ferramentas sinalizam o erro. Por exemplo, um erro difícil de detetar num ficheiro de texto básico é a utilização de letras O maiúsculas/minúsculas em vez de zeros. Se este erro ocorrer, será assinalado. De facto, no código jSON:

"plafondRevenusCouplePourReduction": 42O74

onde, inadvertidamente, foi colocada uma letra «O» maiúscula em vez do zero em [42074], o NetBeans assinala o erro:

Image

De facto, o NetBeans reconhece o O maiúsculo, o que transforma [49O74] numa cadeia de caracteres. Conclui, assim, que a sintaxe deveria ser [4-5]: a cadeia [47O74] deveria estar entre aspas. A atenção do programador é, assim, chamada para o erro e este pode corrigi-lo: quer colocando as aspas, quer substituindo o «O» por um zero;

Os restantes 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 [métier];
  • em [8], os scripts principais 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 do imposto com dados fiscais que passam a provir da base de dados MySQL [dbimpots-2019];

Vamos abordar 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 os dados da administração 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] servirá 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] servirá para encapsular os dados do ficheiro jSON [taxadmindata.json] seguinte:


{
    "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] servirá para encapsular os dados do ficheiro jSON [taxpayerdata.json] seguinte:


[
    {
        "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 das entidades, adotaremos a seguinte regra: os atributos de uma entidade têm os mesmos nomes que os atributos do ficheiro jSON que a entidade deve encapsular. De acordo com esta regra, as entidades [Database, TaxAdminData, TaxPayerData] têm pontos em comum que podem ser fatorizados numa classe pai. Será a seguinte classe [BaseEntity]:


<?php

namespace Application;

class BaseEntity {
  // atributo
  protected $arrayOfAttributes;

  // inicialização a partir de um ficheiro jSON
  public function setFromJsonFile(string $jsonFilename) {
    // recupera-se o conteúdo do ficheiro de dados fiscais
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // erro?
    if (!$fileContents) {
      // regista-se o erro
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // recupera-se o código jSON do ficheiro de configuração numa tabela associativa
      $this->arrayOfAttributes = \json_decode($fileContents, true);
      // erro?
      if ($this->arrayOfAttributes === FALSE) {
        // regista-se o erro
        $erreur = TRUE;
        $message = "Le fichier de données jSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // erro?
    if ($erreur) {
      // lança-se uma exceção
      throw new ExceptionImpots($message);
    }
    // inicialização dos atributos da classe
    foreach ($this->arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // retorna-se o objeto
    return $this;
  }

  public function checkForAllAttributes() {
    // verifica-se se todas as chaves foram inicializadas
    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) {
    // inicializam-se alguns atributos da classe
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // retorna-se o objeto
    return $this;
  }

  // toString
  public function __toString() {
    // atributos do objeto
    $arrayOfAttributes = \get_object_vars($this);
    // remove-se o atributo da classe pai
    unset($arrayOfAttributes["arrayOfAttributes"]);
    // cadeia JSON do objeto
    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] é um tabulácio que contém todos os atributos da classe filha que estendeu a [BaseEntity], bem como os respetivos valores;
  • linhas 9-41: o atributo [$arrayOfAttributes] é inicializado a partir do ficheiro jSON [$jsonFilename] passado como parâmetro. É lançada uma exceção do tipo [ExceptionImpot] se o ficheiro jSON não puder ser lido ou se não for um ficheiro jSON válido;
  • linhas 36-38: trata-se de um código especial caso seja executado por uma classe filha. Neste caso, [$this] representa uma instância da classe filha [Database, TaxAdminData, TaxPayerData] e, nesse caso, as linhas 36-38 inicializam os atributos dessa classe filha, desde que esses atributos tenham a visibilidade protected (ou public) (ver parágrafo com link). De facto, foi referido que os atributos das entidades [Database, TaxAdminData, TaxPayerData] eram os mesmos que os atributos do ficheiro jSON que encapsulavam. Por fim, o método [setFromJsonFile] permite que uma classe filha seja inicializada a partir de um ficheiro jSON;
  • linha 40: devolve-se o objeto [$this], ou seja, uma instância de uma 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 parte dos seus atributos a partir de um tabuleiro associativo cujas chaves têm os mesmos nomes que os atributos da classe filha a ser inicializada;
  • linhas 63-70: o método [__toString] permite obter 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 {
  // atributos
  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 ficheiro jSON [database.json] seguinte:


{
    "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 da base de dados DSN
id
Proprietário da base de dados
pwd
A sua palavra-passe
tableTranches
Nome da tabela que contém as faixas de tributação
colLimites
colCoeffR
colCoeffN
Nomes das colunas da tabela [tableTranches]
tableConstantes
Nome da tabela que contém as constantes de cálculo do imposto
colPlafondQfDemiPart
colPlafondRevenusCelibatairePourReduction
colPlafondRevenusCouplePourReduction
colValeurReducDemiPart
colPlafondDecoteCelibataire
colPlafondDecoteCouple
colPlafondImpotCelibatairePourDecote
colPlafondImpotCouplePourDecote
colAbattementDixPourcentMax
colAbattementDixPourcentMin
Nomes das colunas da tabela [tableConstantes] que contêm as constantes de cálculo do imposto

Por que razão nomear as tabelas e as colunas, se já se conhecem os seus nomes e isso não é algo que venha a mudar? Depois das tabelas SGBD e MySQL, vamos utilizar as tabelas SGBD e PostgreSQL para armazenar os dados da administração fiscal. No entanto, os nomes das colunas e tabelas do Postgres não seguem as mesmas regras que o MySQL. Seremos obrigados a utilizar outros nomes. O mesmo se aplica a outros SGBD. Se quisermos ter código portável entre SGBD, é então preferível utilizar parâmetros em vez dos nomes fixos das tabelas e colunas.

Voltemos ao código da classe [Database]:


<?php

namespace Application;

class Database extends BaseEntity {
  // atributos
  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
  // inicialização
  public function setFromJsonFile(string $jsonFilename) {
    // pai
    parent::setFromJsonFile($jsonFilename);
    // verifica-se se todos os atributos foram inicializados
    parent::checkForAllAttributes();
    // retorna o objeto
    return $this;
  }

  // getters e 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 a visibilidade [protected]. Esta é a condição para que possam ser modificados a partir da classe pai [BaseEntity] (ver parágrafo «ligação»);
  • linhas 28-35: o método [setFromJsonFile] permite inicializar os atributos da classe [Database] a partir do conteúdo de um ficheiro jSON passado como parâmetro. Os atributos do ficheiro jSON e os da classe [Database] têm de ser idênticos. Se o ficheiro jSON não for utilizável, é lançada uma exceção;
  • linha 30: é a classe pai que realiza a inicialização;
  • linha 32: solicita-se à classe pai que verifique se todos os atributos da classe [Database] foram inicializados. Caso contrário, é lançada uma exceção;
  • linha 34: devolve-se a instância [Database] que acabou de ser inicializada;
  • linhas 37 e seguintes: os getters e setters dos atributos da classe;

13.3.2.3. A entidade [TaxAdminData]

A entidade [TaxAdminData] é a seguinte:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // faixas de imposto
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // constantes de cálculo do imposto
  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 ficheiro jSON [taxadmindata.json] a seguir:


{
    "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 restante código da classe [TaxAdminData] é o seguinte:


<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // faixas de imposto
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // constantes de cálculo do imposto
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // inicialização
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // pai
    parent::setFromJsonFile($taxAdminDataFilename);
    // verifica-se se todos os atributos foram inicializados
    parent::checkForAllAttributes();
    // verifica-se se os valores dos atributos são números reais >=0
    foreach ($this as $key => $value) {
      if ($key !== "arrayOfAttributes") {
        // $value deve ser um número real >=0 ou uma matriz de números reais >=0
        $result = $this->check($value);
        // erro?
        if ($result->erreur) {
          // é lançada uma exceção
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // regista-se o valor
          $this->$key = $result->value;
        }
      }
    }
    // retorna-se o objeto
    return $this;
  }

  protected function check($value): \stdClass {
    // $value é um array de elementos do tipo string ou um único elemento
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // transforma-se a matriz de strings numa matriz de números reais
    $newTableau = [];
    $result = new \stdClass();
    // os elementos da matriz devem ser números decimais positivos ou nulos
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // coloca-se o número de tipo float em newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // regista-se o erro
        $result->erreur = TRUE;
        // sai-se
        return $result;
      }
    }
    // retornamos o resultado
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // um único valor
      $result->value = $newTableau[0];
    } else {
      // uma lista de valores
      $result->value = $newTableau;
    }
    return $result;
  }

  // getters e setters

}

Comentários

  • linha 23: o método [setFromJsonFile] serve para inicializar os atributos da classe [TaxAdminData] a partir de um ficheiro jSON passado como parâmetro. É necessário que os atributos do ficheiro jSON existam com o mesmo nome na classe;
  • linha 25: é a classe pai que realiza esta tarefa;
  • linha 27: solicita-se à classe pai que verifique se todos os atributos da classe filha foram inicializados;
  • linhas 29-42: verifica-se localmente se todos os atributos têm um valor real positivo ou nulo. Esta verificação já foi abordada no parágrafo «ligação» da versão 03;

13.3.3. A camada [dao]

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

Image

Image

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


<?php

// espaço de nomes
namespace Application;

interface InterfaceDao4TransferAdminData2Database {

  public function transferAdminData2Database(): void;
}

Comentários

  • linha 8: o método [transferAdminData2Database] tem como função armazenar os dados da administração fiscal numa base de dados;

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


<?php

// espaço de nomes
namespace Application;

// definição de uma classe TransferAdminDataFromFile2DatabaseDao
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database {
  // atributos da base de dados de destino
  private $database;
  // dados da administração fiscal
  private $taxAdminData;

  // fabricante
  public function __construct(string $databaseFilename, string $taxAdminDataFilename) {
    // a configuração da base de dados é guardada
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // os dados fiscais são guardados
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

  // transfere os dados das faixas de imposto de um ficheiro de texto
  // para a base de dados
  public function transferAdminData2Database(): void {
    // trabalha-se na base de dados
    $database = $this->database;
    try {
      // abre-se a ligação à base de dados
      $connexion = new \PDO($database->getDsn(), $database->getId(), $database->getPwd());
      // pretende-se que, sempre que ocorrer um erro no SGBD, seja lançada uma exceção
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // iniciamos uma transação
      $connexion->beginTransaction();
      // preenche-se a tabela de escalões de imposto
      $this->fillTableTranches($connexion);
      // preenche-se a tabela de constantes
      $this->fillTableConstantes($connexion);
      // a transação é concluída com sucesso
      $connexion->commit();
    } catch (\PDOException $ex) {
      // existe alguma transação em curso?
      if (isset($connexion) && $connexion->inTransaction()) {
        // encerra-se a transação com falha
        $connexion->rollBack();
      }
      // a exceção é reenviada ao código chamador
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // encerra-se a ligação
      $connexion = NULL;
    }
  }


  // preenchimento da tabela de escalões de imposto
  private function fillTableTranches($connexion): void {

  }

  // preenchimento da tabela de constantes
  private function fillTableConstantes($connexion): void {

  }

}

Comentários

Aqui, aplicamos o que aprendemos no capítulo sobre a 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] é o objeto do tipo [TaxAdminData] que encapsula os dados do ficheiro [taxadmindata.json];
  • linhas 14-19: o construtor recebe como parâmetros os nomes dos ficheiros [database.json, taxadmindata.json];
  • linha 16: inicialização do atributo [$database];
  • linha 18: inicialização do atributo [$taxAdminData];
  • linha 23: implementa-se o único método da interface [InterfaceDao4TransferAdminData2Database];
  • linhas 26-38: preenche-se a tabela [tbtranches, tbconstantes] em duas etapas:
    • linha 34: preenche-se primeiro a tabela [tbtranches]. Isto é feito no âmbito 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 prossegue com o bloco catch / finally das linhas 39-50;
    • linha 36: preenche-se a tabela [tbconstantes] da mesma forma, utilizando o método [fillTableConstantes] (linha 60);
  • linhas 39-47: caso em que tenha sido lançada uma exceção pelo código;
  • linhas 41-44: se existir uma transação, esta é anulada;
  • linha 46: é lançada uma exceção do tipo [ExceptionImpots] com a mensagem da exceção original, que, por sua vez, é de um tipo qualquer;
  • 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 {
    // atalho para a base de dados
    $database = $this->database;
    // os dados a inserir na base de dados
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // esvazia-se a tabela, caso haja algo nela
    $statement = $connexion->prepare("delete from " . $database->getTableTranches());
    $statement->execute();
    // prepara-se as inserções
    $sqlInsert = "insert into {$database->getTableTranches()} "
      . "({$database->getColLimites()}, {$database->getColCoeffR()},"
      . " {$database->getColCoeffN()}) values (:limites, :coeffR, :coeffN)";
    $statement = $connexion->prepare($sqlInsert);
    // executa-se a ordem preparada com os valores das faixas de imposto
    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 como parâmetro uma ligação aberta. Sabe-se ainda que foi iniciada uma transação no âmbito dessa ligação;
  • linhas 5-7: os valores a inserir na tabela são fornecidos pelo atributo [$taxAdminData];
  • linhas 9-10: elimina-se o conteúdo atual da tabela [tbtranches];
  • linhas 12-15: prepara-se a inserção de linhas na tabela. Aqui, utilizam-se os nomes das colunas fornecidos pelo atributo [$database];
  • linhas 17-22: executa-se, tantas vezes quantas forem necessárias, a instrução de inserção preparada nas linhas 12-15;

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


private function fillTableConstantes($connexion): void {
    // atalho
    $database = $this->database;
    // esvazia-se a tabela, caso haja algo nela
    $statement = $connexion->prepare("delete from {$database->getTableConstantes()}");
    $statement->execute();
    // prepara-se a inserção
    $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);
    // executa-se a ordem preparada
    $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 como parâmetro uma ligação aberta. Sabe-se ainda que foi iniciada uma transação no âmbito dessa ligação;
  • linhas 5-6: a tabela [tbconstantes] é esvaziada;
  • linhas 9-31: preparação da ordem de inserção SQL. É complexa, uma vez que há 10 colunas a inicializar nesta operação de inserção e é necessário ir buscar os nomes das colunas no atributo [$database];
  • linhas 33-44: execução da ordem de inserção. Há apenas uma linha a inserir. Mais uma vez, o código torna-se complexo devido à necessidade de procurar os valores a inserir no atributo [$taxAdminData];

13.3.4. O script principal

Image

Image

O script principal baseia-se na camada [dao] para efetuar a transferência de dados:


<?php

// respeito rigoroso dos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Application;

// gestão de erros por PHP
// ini_set("display_errors", "0");
// inclusão de interfaces e classes
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";
//
// definição das constantes
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";

//
try {
  // criação da camada [dao]
  $dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
  // transferência de dados para a base de dados
  $dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
  // exibição do erro
  print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fim
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: instanciamos a camada [dao], passando ao construtor os dois ficheiros jSON;
  • linha 32: efetua-se a transferência de dados;

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

Image

Na coluna [3], vemos os valores atribuídos por MySQL à chave primária [id]. A numeração começa em 1. A captura de ecrã acima foi obtida após várias execuções do script.

Image

Image

13.4. Cálculo do imposto

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 essa interface:

  • [DaoImpotsWithTaxAdminDataInJsonFile], que iria buscar os dados fiscais a um ficheiro jSON. Esta era a versão 04;

Vamos implementar a interface [InterfaceDao] através de uma nova classe [DaoImpotsWithTaxAdminDataInDatabase], que irá buscar os dados da administração fiscal numa base de dados MySQL. A camada [dao], tal como anteriormente, irá gravar os resultados e os erros em ficheiros de texto e irá também obter os dados dos contribuintes a partir de um ficheiro de texto. Só que, desta vez, esses ficheiros de texto serão ficheiros jSON. Além disso, sabemos que, se continuarmos a respeitar a interface [InterfaceDao], a camada [métier] não terá de ser alterada.

Image

13.4.2. A entidade [TaxPayerData]

Image

A classe [TaxPayerData] serve para encapsular numa classe os dados do ficheiro jSON [taxpayersdata.json] seguinte:


[
    {
        "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

// espaço de nomes
namespace Application;

// a classe dos dados
class TaxPayerData extends BaseEntity {
  // dados necessários para o cálculo do imposto do contribuinte
  protected $marié;
  protected $enfants;
  protected $salaire;
  // resultados do cálculo do imposto
  protected $impôt;
  protected $surcôte;
  protected $décôte;
  protected $réduction;
  protected $taux;

  // getters e 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. Recorde-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]

O traço [TraitDao] implementa uma parte da interface [InterfaceDao]. Recorde-se que esta é a seguinte:


<?php

// espaço de nomes
namespace Application;

interface InterfaceDao {

  // leitura dos dados dos contribuintes
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // leitura dos dados da administração fiscal (faixas de imposto)
  public function getTaxAdminData(): TaxAdminData;

  // registo dos resultados
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

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


<?php

// espaço de nomes
namespace Application;

trait TraitDao {

  // leitura dos dados dos contribuintes
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // recuperação dos dados dos contribuintes numa tabela
    $baseEntity = new BaseEntity();
    $baseEntity->setFromJsonFile($taxPayersFilename);
    $arrayOfAttributes = $baseEntity->getArrayOfAttributes();
    // tabela de dados dos contribuintes
    $taxPayersData = [];
    // tabela de erros
    $errors = [];
    // iteração sobre a tabela de atributos de elementos do tipo [TaxPayerData]
    $i = 0;
    foreach ($arrayOfAttributes as $attributesOfTaxPayerData) {
      // verificação
      $error = $this->check($attributesOfTaxPayerData);
      if (!$error) {
        // um contribuinte com +
        $taxPayersData[] = (new TaxPayerData())->setFrOmArrayOfAttributes($attributesOfTaxPayerData);
      } else {
        // um erro de + - regista-se o número do dado inválido
        $error = ["numéro" => $i] + $error;
        $errors[] = $error;
      }
      // seguinte
      $i++;
    }
    // guardamos os erros num ficheiro JSON
    $string = "";
    foreach ($errors as $error) {
      $string .= \json_encode($error, JSON_UNESCAPED_UNICODE) . "\n";
    }
    $this->saveString($errorsFilename, $string);
    // resultado da função
    return $taxPayersData;
  }

  private function check(array $attributesOfTaxPayerData): array {
    // verificam-se os dados de [$taxPayerData]
    // a lista de atributos com erros
    $attributes = [];
    // o estado civil deve ser «sim» ou «não»
    $marié = trim(strtolower($attributesOfTaxPayerData["marié"]));
    $erreur = ($marié !== "oui" and $marié !== "non");
    if ($erreur) {
      // regista-se o erro
      $attributes[] = ["marié" => $marié];
    }
    // o número de filhos deve ser um número inteiro positivo ou zero
    $enfants = trim($attributesOfTaxPayerData["enfants"]);
    if (!preg_match("/^\d+$/", $enfants)) {
      // regista-se o erro
      $erreur = TRUE;
      $attributes[] = ["enfants" => $enfants];
    } else {
      $enfants = (int) $enfants;
    }

    // o salário deve ser um número inteiro positivo ou zero (sem cêntimos de euro)
    $salaire = trim($attributesOfTaxPayerData["salaire"]);
    if (!preg_match("/^\d+$/", $salaire)) {
      // regista-se o erro
      $erreur = TRUE;
      $attributes[] = ["salaire" => $salaire];
    } else {
      $salaire = (int) $salaire;
    }

    // erro?
    if ($erreur) {
      // resposta com erro
      return ["erreurs" => $attributes];
    } else {
      // resposta sem erro
      return [];
    }
  }

  // registo dos resultados
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    // gravação da tabela [$taxPayersData] no ficheiro de texto [$resultsFileName]
    // se o ficheiro de texto [$resultsFileName] não existir, é criado
    // construção da cadeia jSON a partir dos resultados
    $string = "[" . implode(",
", $taxPayersData) . "]";
    // gravação desta cadeia
    $this->saveString($resultsFilename, $string);
  }

  // registo dos resultados de uma tabela num ficheiro de texto
  private function saveString(string $fileName, string $data): void {
    // gravação da cadeia [$data] no ficheiro de texto [$fileName]
    // se o ficheiro de texto [$fileName] não existir, é criado
    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] recebe como parâmetros:
    • [$taxPayersFilename]: o nome do ficheiro jSON com os dados dos contribuintes [taxpayersdata.json];
    • [$errorsFilename]: o nome do ficheiro jSON dos erros [errors.json];
  • linhas 11-13: o conteúdo do ficheiro jSON com os dados dos contribuintes é transferido para uma tabela associativa [$arrayOfAttributes]. Se o ficheiro jSON se revelar inutilizável, foi lançada uma exceção [ExceptionImpots];
  • linha 15: a tabela [$taxPayersData] irá conter os dados dos contribuintes encapsulados em objetos do tipo [TaxPayerData];
  • linha 17: os erros serão acumulados na tabela [$errors];
  • linhas 99-33: construção da tabela [$taxPayersData];
  • linha 22: antes de serem encapsulados num tipo [TaxPayerData], os dados são verificados. O método [check] devolve:
    • um array [‘erreurs’=>[…]] com os atributos incorretos, caso os dados estejam incorretos;
    • um array vazio, se os dados estiverem corretos;
  • linha 25: caso em que os dados são válidos. É criado um novo objeto [TaxPayerData] e adicionado ao array [$taxPayersData];
  • linhas 26-30: caso em que os dados são inválidos. No erro, é indicado o n.º do objeto [TaxPayerData] com erro no ficheiro jSON, para que o utilizador o possa localizar; em seguida, o erro é adicionado à tabela [$errors];
  • linhas 35-39: registam-se os erros encontrados no ficheiro jSON [$errorsFilename] passado como parâmetro, linha 9;
  • linha 41: devolve-se a tabela dos objetos [TaxPayerData] criados: este era o objetivo do método;
  • linhas 44-83: o método privado [check] verifica a validade dos parâmetros [marié, enfants, salaire] da tabela [$attributesOfTaxPayerData] passada como parâmetro na linha 44. Se houver atributos errados, o método acumula-os na tabela [$attributes] (linhas 47, 53, 60, 70) sob a forma de uma tabela [‘attribut erroné’=> valeur de l’attribut erroné];
  • linha 78: se houver erros, é devolvida uma tabela [‘erreurs’=>$attributes];
  • linha 81: se não houver erros, devolve-se uma tabela de erros vazia;
  • linhas 86-93: implementação do método [saveResults] da interface [InterfaceDao];
  • linha 90: constrói-se a cadeia jSON a gravar no ficheiro jSON [$resultsFilename] passado como parâmetro na linha 86. Deve-se construir a cadeia jSON a partir de um tabuleiro:
    • cada elemento da tabela é separado do seguinte por uma vírgula e um salto de linha;
    • toda a matriz está entre colchetes [];
  • linha 92: a cadeia jSON é gravada no ficheiro jSON [$resultsFilename];

13.4.3.2. A classe [DaoImpotsWithTaxAdminDataInDatabase]

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


<?php

// espaço de nomes
namespace Application;

// definição de uma classe ImpotsWithDataInDatabase
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao {
  // utilização de uma característica
  use TraitDao;
  // o objeto do tipo TaxAdminData que contém os dados das faixas de imposto
  private $taxAdminData;
  // o objeto do tipo [Database] que contém as características do BD
  private $database;

  // fabricante
  public function __construct(string $databaseFilename) {
    // armazena-se a configuração jSON da base de dados
    $this->database = (new Database())->setFromJsonFile($databaseFilename);
    // prepara-se o atributo
    $this->taxAdminData = new TaxAdminData();
    try {
      // abre-se a ligação à base de dados
      $connexion = new \PDO(
        $this->database->getDsn(),
        $this->database->getId(),
        $this->database->getPwd());
      // pretende-se que, sempre que ocorrer um erro no SGBD, seja lançada uma exceção
      $connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
      // inicia-se uma transação
      $connexion->beginTransaction();
      // preenche-se a tabela de escalões de imposto
      $this->getTranches($connexion);
      // preenche-se a tabela de constantes
      $this->getConstantes($connexion);
      // a transação é concluída com sucesso
      $connexion->commit();
    } catch (\PDOException $ex) {
      // existe alguma transação em curso?
      if (isset($connexion) && $connexion->inTransaction()) {
        // encerra-se a transação com falha
        $connexion->rollBack();
      }
      // a exceção é reenviada para o código chamador
      throw new ExceptionImpots($ex->getMessage());
    } finally {
      // encerra-se a ligação
      $connexion = NULL;
    }
  }

  // leitura dos dados da base de dados
  private function getTranches($connexion): void {

  }

  // leitura da tabela de constantes
  private function getConstantes($connexion): void {

  }

  // retorna os dados que permitem o cálculo do imposto
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }

}

Comentários

  • linha 4: mantém-se o espaço de nomes já utilizado para as outras implementações da camada [dao];
  • linha 7: a classe [DaoImpotsWithTaxAdminDataInDatabase] implementa a interface [InterfaceDao];
  • linha 9: importa-se o traço [TraitDao]. Sabe-se que este traço implementa uma parte da interface. O único método que resta implementar é o método [getTaxAdminData] das linhas 62-64. Este método limita-se a definir o atributo privado [taxAdminData] da linha 11. Deduz-se que o construtor terá de inicializar este atributo. É a sua única função;
  • linha 16: o construtor recebe como ú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 do tipo [Database], construído e armazenado no atributo [$database] da linha 13. Se o ficheiro jSON não tiver podido ser processado corretamente, foi lançada uma exceção [ExceptionImpots];
  • linha 20: cria-se o objeto [$this→taxAdminData], que o construtor deve inicializar;
  • linhas 22-26: abre-se a ligação à base de dados. Repare na notação [\PDO] para designar a classe [PDO] de PHP. Com efeito, como nos encontramos no espaço de nomes [Application], se escrevêssemos simplesmente [PDO], esse nome relativo seria prefixado pelo espaço de nomes atual e resultaria, portanto, na classe [Application\PDO], que não existe;
  • linha 28: em caso de erro, o SGBD irá lançar um \PDOException (linha 37);
  • linha 30: inicia-se uma transação. Esta não é realmente útil, uma vez que apenas serão executadas duas ordens SQL, ordens que não alteram a base de dados. No entanto, faz-se isto para nos isolarmos dos outros utilizadores da base de dados;
  • linha 32: a leitura da tabela de escalões de tributação [tbtranches] é efetuada pelo método privado [getTranches] da linha 52;
  • linha 34: a leitura da tabela de constantes de cálculo [tbconstantes] é efetuada pelo método privado [getConstantes] da linha 57;
  • linha 36: se chegarmos a esta linha, significa que tudo correu bem. Validamos, portanto, a transação;
  • linhas 37-42: se chegarmos até aqui, significa que ocorreu uma exceção. Por isso, invalida-se a transação, caso houvesse alguma em curso (linhas 39-42). Linha 44: para garantir que as exceções sejam homogéneas, reenvia-se a mensagem da exceção recebida, desta vez sob a forma de uma exceção do tipo [ExceptionImpots];
  • linhas 45-48: em todos os casos (haja ou não exceção), encerra-se a ligação;

O método [getTranches] é o seguinte:


private function getTranches($connexion): void {
     // atalhos
    $database = $this->database;
    $taxAdminData = $this->taxAdminData;
     // prepara-se a consulta SELECT
    $statement = $connexion->prepare(
      "select {$database->getColLimites()}," .
      " {$database->getColCoeffR()}," .
      " {$database->getColCoeffN()}" .
      " from {$database->getTableTranches()}");
     // executa-se a ordem preparada com os valores das faixas de imposto
    $statement->execute();
     // análise do resultado
    $limites = [];
    $coeffR = [];
    $coeffN = [];
     // preenchimento das três tabelas
    while ($tranche = $statement->fetch(\PDO::FETCH_OBJ)) {
      $limites[] = (float) $tranche->{$database->getColLimites()};
      $coeffR[] = (float) $tranche->{$database->getColCoeffR()};
      $coeffN[] = (float) $tranche->{$database->getColCoeffN()};
    }
    // os dados são armazenados no atributo [$taxAdminData] da classe
    $taxAdminData->setFromArrayOfAttributes([
      "limites" => $limites,
      "coeffR" => $coeffR,
      "coeffN" => $coeffN
    ]);
  }

Comentários

  • linha 1: o método recebe como parâmetro [$connexion], que é uma ligação aberta e na qual está em curso uma transação;
  • linhas 2-4: criam-se dois atalhos para evitar ter de escrever [$this->database] e [$taxAdminData = $this->taxAdminData] em todo o código. Trata-se aqui de cópias de referências a objetos e não de uma cópia dos próprios objetos;
  • linhas 6-10: a ordem SELECT é preparada e, em seguida, executada na linha 12;
  • linhas 13-22: o resultado do SELECT é processado. As informações recebidas são acumuladas em três tabelas [limites, coeffR, coeffN];
  • linhas 24-28: as três tabelas são utilizadas para inicializar o atributo [$this->taxAdminData] da classe;

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


private function getConstantes($connexion): void {
    // atalhos
    $database = $this->database;
    $taxAdminData = $this->taxAdminData;
    // prepara-se a consulta 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);
    // executa-se a ordem preparada
    $statement->execute();
    // analisamos o resultado — apenas uma linha aqui
    $row = $statement->fetch(\PDO::FETCH_OBJ);
    // inicializa-se o atributo [$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 como parâmetro [$connexion], que é uma ligação aberta e na qual está em curso uma transação;
  • linhas 2-4: criam-se dois atalhos para evitar ter de escrever [$this->database] e [$taxAdminData = $this->taxAdminData] em todo o código. Trata-se aqui de cópias de referências a objetos e não de uma cópia dos próprios objetos;
  • linhas 6-15: a ordem SELECT é preparada e, em seguida, executada na linha 15;
  • linhas 17-29: o resultado de SELECT é processado. As informações recuperadas são utilizadas para inicializar o atributo [$this->taxAdminData] da classe;

Nota: note-se que a classe não depende do SGBD nem do MySQL. É o código chamador que define o SGBD utilizado através do DSN da base de dados.

13.4.4. A camada [métier]

Image

  • Acabámos de implementar a camada [dao] (3);
  • uma vez que respeitámos a interface [InterfaceDao], a camada [métier] (2) pode, em teoria, permanecer inalterada. No entanto, não nos limitámos a modificar apenas a camada [dao]. Também alterámos as entidades que são partilhadas por todas as camadas;

A camada [métier] implementa a seguinte interface [InterfaceMetier]:


<?php

// espaço de nomes
namespace Application;

interface InterfaceMetier {

  // cálculo dos impostos de um contribuinte
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // cálculo de impostos em modo batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
  • linha 12: o método [executeBatchImpots] 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 {
    // permitir que as exceções provenientes da camada [dao] sejam reenviadas
    // recuperam-se os dados dos contribuintes
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela de resultados
    $results = [];
    // analisam-se os resultados
    foreach ($taxPayersData as $taxPayerData) {
      // calcula-se o imposto
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // preenche-se [$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"]);
      // insere-se o resultado na tabela de resultados
      $results [] = $taxPayerData;
    }
    // registo dos resultados
    $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 {
    // deixa-se que as exceções provenientes da camada [dao] sejam reportadas
    // recuperam-se os dados dos contribuintes
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tabela de resultados
    $results = [];
    // analisam-se os resultados
    foreach ($taxPayersData as $taxPayerData) {
      // calcula-se o imposto
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // preenche-se [$taxPayerData]
      $taxPayerData->setFromArrayOfAttributes($result);
      // insere-se o resultado na tabela de resultados
      $results [] = $taxPayerData;
    }
    // registo dos resultados
    $this->dao->saveResults($resultsFileName, $results);
  }

Comentários

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

13.4.5. O script principal

Image

  • Acabámos de implementar as camadas [dao] (3) e [métier] (2);
  • resta-nos escrever o script principal (1);

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


<?php

// respeito rigoroso dos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Application;

// gestão de erros por PHP
//ini_set("display_errors", "0");
// inclusão de interface e classes
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";
//
// definição das constantes
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 {
  // criação da camada [dao]
  $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
  // criação da camada [métier]
  $métier = new Metier($dao);
  // cálculo dos impostos em modo batch
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // exibição do erro
  print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fim
print "Terminé\n";
exit;


Comentários

  • linhas 12-22: carregamento de todos os ficheiros da versão 05;
  • linhas 25-29: os nomes dos diferentes ficheiros jSON da aplicação;
  • linha 33: construção da camada [dao];
  • linha 35: construção da camada [métier];
  • linha 37: chamada do método [executeBatchImpots] da camada [métier];

Resultados

A aplicação gera dois ficheiros jSON:

  • [resultats.json]: os resultados dos diferentes cálculos de impostos;
  • [errors.json]: que assinala os erros encontrados nos ficheiros jSON e [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], o elemento n.º 1 da tabela de contribuintes está errado. O ficheiro [taxpayersdata.json] era o seguinte:


[
    {
        "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 [resultats.json] é, por sua vez, 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 estão em conformidade com os da versão 04.

13.5. Testes [Codeception]

Tal como foi feito no parágrafo relativo à versão 04, vamos escrever testes [Codeception] para a versão 05.

Image

13.5.1. Teste da camada [dao]

O teste [DaoTest.php] é o seguinte:


<?php

// respeito rigoroso dos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Application;

// diretórios raiz
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");

// inclusão de interfaces e classes
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";
// bibliotecas de terceiros
require_once VENDOR . "/autoload.php";

// definição de constantes
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();
    // criação da camada [dao]
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // testes
  public function testTaxAdminData() {
    // constantes de cálculo
    $this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());

  }

}

Comentários

  • linhas 9-33: definição do ambiente de teste. Utilizamos o mesmo ambiente que o utilizado pelo script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase], descrito no parágrafo 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 no parágrafo «ligação»;

Os resultados do teste são os seguintes:

Image

13.5.2. Teste da camada [métier]

O teste [MetierTest.php] é o seguinte:


<?php

// respeito rigoroso dos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Application;

// diretórios raiz
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");

// inclusão de interfaces e classes
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";
// bibliotecas de terceiros
require_once VENDOR . "/autoload.php";

// definição de constantes
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 {
  // camada de negócio
  private $métier;

  public function __construct() {
    parent::__construct();
    // criação da camada [dao]
    $dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
    // criação da camada [métier]
    $this->métier = new Metier($dao);
  }

  // testes
  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 ambiente que o utilizado pelo script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase], descrito no parágrafo com o link;
  • linhas 39-45: construção das camadas [dao] e [métier];
  • linha 44: o atributo [$this→métier] faz referência à camada [métier];
  • linhas 47-64: os métodos [test1, test2…, test11] são os descritos no parágrafo «ligação»;

Os resultados do teste são os seguintes:

Image