13. Esercizio pratico – Versione 5

Abbiamo già scritto diverse versioni di questo esercizio. L'ultima versione utilizzava un'architettura a livelli:

Il livello [dao] implementa un'interfaccia [InterfaceDao]. Abbiamo creato una classe che implementa questa interfaccia:
- [DaoImpotsWithTaxAdminDataInJsonFile], che recuperava i dati fiscali da un file JSON;
Implementeremo l'interfaccia [InterfaceDao] con una nuova classe [DaoImportsWithTaxAdminDataInDatabase] che recupererà i dati dell'amministrazione fiscale da un database MySQL.
13.1. Creazione del database [dbimpots-2019]
Seguendo l'esempio nella sezione "link", creiamo un database MySQL denominato [dbimpots-2019], di proprietà di [admimpots] con la password [mdpimpots]:

- Nel punto [1-4] sopra riportato, vediamo il database [dbimpots-2019], che attualmente non contiene tabelle;

- In [1-5] sopra, vediamo che l'utente [admimpots] dispone di privilegi completi sul database [dbimpots-2019]. Ciò che non vediamo qui è che questo utente ha la password [admimpots];
Ora creeremo la tabella [tbtranches], che conterrà le fasce di imposta:

- In [1-7], creiamo una tabella denominata [tbtranches] con 4 colonne;

- In [3-6], definiamo una colonna denominata [id] (3), di tipo intero [int] (4), che sarà la chiave primaria [6] della tabella e verrà autoincrementata [5] dal DBMS. Ciò significa che MySQL gestirà autonomamente i valori della chiave primaria durante gli inserimenti. Assegnerà il valore 1 alla chiave primaria del primo inserimento, poi 2 a quello successivo e così via;
- in [7], la procedura guidata offre ulteriori opzioni di configurazione per la chiave primaria. Qui, accettiamo semplicemente [7] i valori predefiniti;

- In [8-16], definiamo le altre tre colonne della tabella:
- [limits] (8), un numero decimale (9) di 10 cifre, comprese 2 cifre decimali (10), conterrà gli elementi della colonna 17 delle fasce di imposta;
- [coeffR] (11), un numero decimale a 6 cifre (12) con 2 cifre decimali (13), conterrà i valori della colonna 18 delle fasce di imposta;
- [coeffN] (14) di tipo numero decimale (15) con 10 cifre, incluse 2 cifre decimali (16), conterrà gli elementi della colonna 19 delle fasce di imposta;
Dopo aver convalidato questa struttura, otteniamo il seguente risultato:

- in [5], l'icona della chiave indica che la colonna [id] è la chiave primaria. Possiamo anche vedere che questa chiave primaria ha valori interi (6) ed è gestita (autoincrementata) da MySQL;
Allo stesso modo in cui abbiamo creato la tabella [tbtranches], costruiamo la tabella [tbconstantes], che conterrà le costanti utilizzate nel calcolo delle imposte:

È possibile esportare la struttura del database in un file di testo come sequenza di istruzioni SQL:

L'opzione [5] esporta solo la struttura del database, non il suo contenuto. Nel nostro caso, il database non ha ancora alcun contenuto.



L'opzione [11] genera il seguente file 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 */;
È possibile utilizzare questo file SQL per rigenerare il database [dbimpots-2019] se è stato distrutto o danneggiato. Non è necessario eliminare il database prima di rigenerarlo, poiché lo script SQL gestisce automaticamente questa operazione:


13.2. Organizzazione del codice
Per illustrare meglio il ruolo dei vari script PHP che stiamo scrivendo, organizzeremo il nostro codice in cartelle:

- in [1], panoramica della versione 05;
- in [2], le entità applicative, ovvero le entità scambiate tra i livelli;
- in [3], le utilità dell'applicazione;
- in [4], i dati utilizzati o prodotti dall'applicazione. In questo caso, abbiamo deciso di utilizzare solo file JSON per i file di testo. Questi offrono diversi vantaggi:
- sono riconosciuti da molti strumenti;
- questi strumenti supportano l'evidenziazione della sintassi. Inoltre, JSON ha regole rigide. Quando queste regole non vengono seguite, gli strumenti le segnalano. Ad esempio, un errore comune in un file di testo di base è l'uso della O maiuscola o minuscola al posto degli zeri. Se si verifica questo errore, verrà segnalato. Nel codice JSON:
"plafondRevenusCouplePourReduction": 42O74
dove è stata inavvertitamente utilizzata una O maiuscola al posto di uno zero in [42074], NetBeans segnala l'errore:

Infatti, NetBeans riconosce la O maiuscola, il che rende [49O74] una stringa. Conclude che la sintassi dovrebbe essere [4-5]: la stringa [47O74] dovrebbe essere racchiusa tra virgolette. L'attenzione dello sviluppatore viene così attirata sull'errore e può correggerlo: aggiungendo le virgolette o sostituendo la O con uno zero;
Gli altri elementi della versione 05 sono i seguenti:

- in [6], le interfacce e le classi del livello [Dao];
- in [7], le interfacce e le classi del livello [business];
- in [8], gli script principali della versione 05;
La versione 05 ha due obiettivi distinti:
- popolare il database MySQL [dbimpots-2019] con i contenuti del file JSON [Data/txadmindata.json];
- implementare il calcolo delle imposte utilizzando i dati fiscali ora provenienti dal database MySQL [dbimpots-2019];
Affronteremo questi due obiettivi separatamente.
13.3. Popolamento del database [dbimpots-2019]
13.3.1. Obiettivo
Il file di testo taxadmindata.json contiene i dati provenienti dall'autorità fiscale:
{
"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
}
Il nostro obiettivo è trasferire questi dati nel database MySQL [dbimpots-2019] creato in precedenza.
13.3.2. Le entità

L'entità [Database] verrà utilizzata per incapsulare i dati provenienti dal seguente file 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"
}
L'entità [TaxAdminData] verrà utilizzata per incapsulare i dati del seguente file 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
}
L'entità [TaxPayerData] verrà utilizzata per incapsulare i dati del seguente file 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. La classe base [BaseEntity]
Per semplificare il codice delle entità, adotteremo la seguente regola: gli attributi di un'entità hanno gli stessi nomi degli attributi nel file JSON che l'entità è destinata a incapsulare. In base a questa regola, le entità [Database, TaxAdminData, TaxPayerData] condividono caratteristiche comuni che possono essere raggruppate in una classe padre. Questa sarà la seguente 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;
}
}
Commenti
- riga 5: la classe [BaseEntity] è destinata ad essere estesa dalle classi [Database, TaxAdminData, TaxPayerData];
- riga 7: l'attributo [$arrayOfAttributes] è un array contenente tutti gli attributi della classe figlia che estende [BaseEntity] insieme ai relativi valori;
- righe 9–41: l'attributo [$arrayOfAttributes] viene inizializzato dal file JSON [$jsonFilename] passato come parametro. Viene generata un'eccezione [ExceptionImpot] se il file JSON non può essere letto o se non è un file JSON valido;
- righe 36–38: si tratta di codice speciale se eseguito da una classe figlia. In questo caso, [$this] rappresenta un'istanza della classe figlia [Database, TaxAdminData, TaxPayerData] e, in questo scenario, le righe 36–38 inizializzano gli attributi di questa classe figlia, a condizione che tali attributi abbiano visibilità protetta (o pubblica) (vedere la sezione collegata). Abbiamo notato che gli attributi delle entità [Database, TaxAdminData, TaxPayerData] sono gli stessi degli attributi del file JSON che incapsulano. Infine, il metodo [setFromJsonFile] consente a una classe figlia di inizializzarsi da un file JSON;
- riga 40: l'oggetto [$this] viene impostato su un'istanza della classe figlia se il metodo [setFromJsonFile] è stato chiamato da una classe figlia;
- righe 43–51: il metodo [checkForAllAttributes] consente a una classe figlia di verificare che tutti i suoi attributi siano stati inizializzati. In caso contrario, viene generata un'eccezione [ExceptionImpots]. Questo metodo consente alla classe figlia di verificare che il suo file JSON non abbia omesso determinati attributi;
- righe 53–60: il metodo [setFromArrayOfAttributes] consente a una classe figlia di inizializzare tutti o alcuni dei propri attributi da un array associativo le cui chiavi hanno gli stessi nomi degli attributi della classe figlia da inizializzare;
- righe 63–70: Il metodo [__toString] fornisce la rappresentazione JSON di una classe figlia;
13.3.2.2. L'entità [Database]
L'entità [Database] è la seguente:
<?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;
…
}
La classe [Database] viene utilizzata per incapsulare i dati del seguente file 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"
}
La classe e il file JSON hanno gli stessi attributi. Questi descrivono le caratteristiche del database MySQL [dbimpots-2019]:
dsn | Nome DSN del database |
id | Proprietario del database |
pwd | La loro password |
tableTranches | Nome della tabella contenente le fasce di imposta |
colLimits colRate colCoeffN | Nomi delle colonne nella tabella [tableTranches] |
tableConstants | Nome della tabella contenente le costanti di calcolo delle imposte |
colIncomeCeilingForHalfShare colIncomeLimitSingleForReduction colIncomeLimitCoupleForReduction colValoreRiduzioneMetàQuota colLimiteCreditoD'impostaSingolo colMassimaleDeduzioneCoppia colLimiteMassimoRedditoSingoloPerDetrazione colMassimaleFiscaleCoppiaPerDeduzione colDeduzioneMassimaDelDieciPercento colDeduzioneMinimaDiDieciPercento | Nomi delle colonne nella tabella [tableConstants] contenente le costanti di calcolo delle imposte |
Perché dare un nome alle tabelle e alle colonne quando ne conosciamo già i nomi e non è probabile che cambino? Dopo il DBMS MySQL, useremo il DBMS PostgreSQL per memorizzare i dati dell'amministrazione fiscale. Tuttavia, i nomi delle colonne e delle tabelle di Postgres non seguono le stesse regole di MySQL. Saremo costretti a usare nomi diversi. Questo vale anche per altri DBMS. Se vogliamo un codice che sia portabile tra i DBMS, è preferibile usare parametri piuttosto che nomi di tabelle e colonne hard-coded.
Torniamo al codice della 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;
}
…
}
Commenti
- righe 7–24: tutti gli attributi della classe hanno visibilità [protected]. Questo è un requisito affinché possano essere modificati dalla classe padre [BaseEntity] (vedi la sezione collegata);
- righe 28–35: il metodo [setFromJsonFile] consente di inizializzare gli attributi della classe [Database] a partire dal contenuto di un file JSON passato come parametro. Gli attributi nel file JSON e quelli nella classe [Database] devono essere identici. Se il file JSON è inutilizzabile, viene generata un'eccezione;
- riga 30: la classe padre esegue l'inizializzazione;
- riga 32: alla classe padre viene chiesto di verificare che tutti gli attributi della classe [Database] siano stati inizializzati. In caso contrario, viene generata un'eccezione;
- riga 34: viene restituita l'istanza [Database] appena inizializzata;
- righe 37 e seguenti: i getter e i setter per gli attributi della classe;
13.3.2.3. L'entità [TaxAdminData]
L'entità [TaxAdminData] è la seguente:
<?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;
…
}
La classe [TaxAdminData] viene utilizzata per incapsulare i dati del seguente file 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
}
La classe e il file JSON hanno gli stessi attributi. Questi rappresentano i dati dell'amministrazione fiscale. Il resto del codice per la classe [TaxAdminData] è il seguente:
<?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
…
}
Commenti
- Riga 23: Il metodo [setFromJsonFile] viene utilizzato per inizializzare gli attributi della classe [TaxAdminData] da un file JSON passato come parametro. Gli attributi nel file JSON devono avere gli stessi nomi di quelli nella classe;
- Riga 25: La classe padre esegue questa operazione;
- riga 27: alla classe padre viene richiesto di verificare che tutti gli attributi della classe figlia siano stati inizializzati;
- righe 29–42: verifichiamo localmente che tutti gli attributi abbiano un valore reale positivo o siano nulli. Questa verifica è già stata discussa nella sezione “link” della versione 03;
13.3.3. Il livello [dao]
Ora possiamo scrivere il codice che trasferirà i dati dal file di testo [taxadmindata.json] nelle tabelle [tbtranches, tbconstantes] del database MySQL [dbimpots-2019]. Adotteremo la seguente architettura:


Il livello [dao] implementerà la seguente interfaccia [InterfaceDao4TransferAdminDataFromFile2Database]:
<?php
// namespace
namespace Application;
interface InterfaceDao4TransferAdminData2Database {
public function transferAdminData2Database(): void;
}
Commenti
- riga 8: il metodo [transferAdminData2Database] è responsabile della memorizzazione dei dati amministrativi fiscali in un database;
L'interfaccia [InterfaceDao4TransferAdminData2Database] sarà implementata dalla seguente 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 {
…
}
}
Commenti
Qui stiamo mettendo in pratica ciò che abbiamo imparato nel capitolo su MySQL.
- riga 7: la classe [DaoTransferAdminDataFromJsonFile2Database] implementa l'interfaccia [InterfaceDao4TransferAdminData2Database];
- riga 9: l'attributo [$database] è l'oggetto di tipo [Database] che incapsula i dati dal file [database.json];
- riga 11: l'attributo [$taxAdminData] è un oggetto di tipo [TaxAdminData] che incapsula i dati dal file [taxadmindata.json];
- righe 14–19: il costruttore riceve i nomi dei file [database.json, taxadmindata.json] come parametri;
- riga 16: inizializzazione dell'attributo [$database];
- riga 18: inizializzazione dell'attributo [$taxAdminData];
- riga 23: viene implementato l'unico metodo dell'interfaccia [InterfaceDao4TransferAdminData2Database];
- Righe 26–38: le tabelle [tbtranches] e [tbconstantes] vengono popolate in due fasi:
- riga 34: Innanzitutto, viene compilata la tabella [tbtranches]. Ciò avviene all’interno di una transazione (righe 32, 38). Il metodo [fillTableTranches] (riga 55) genera un’eccezione non appena si verifica un errore. In questo caso, l’esecuzione prosegue con il blocco catch / finally alle righe 39–50;
- riga 36: la tabella [tbconstantes] viene compilata allo stesso modo utilizzando il metodo [fillTableConstantes] (riga 60);
- righe 39–47: caso in cui il codice ha generato un'eccezione;
- righe 41–44: se esiste una transazione, viene annullata;
- riga 46: viene generata un'eccezione di tipo [ExceptionImpots] con il messaggio dell'eccezione originale, che è di qualsiasi tipo;
- righe 47–50: nella clausola [finally], la connessione viene chiusa;
Il codice del metodo [fillTableTranches] è il seguente:
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]]);
}
}
Commenti
- riga 1: il metodo [fillTableTranches] accetta una connessione aperta come parametro. Sappiamo inoltre che all'interno di questa connessione è stata avviata una transazione;
- righe 5–7: i valori da inserire nella tabella sono forniti dall'attributo [$taxAdminData];
- righe 9–10: il contenuto attuale della tabella [tbtranches] viene cancellato;
- righe 12–15: ci prepariamo a inserire righe nella tabella. Qui utilizziamo i nomi delle colonne forniti dall'attributo [$database];
- righe 17–22: l'istruzione di inserimento preparata nelle righe 12–15 viene eseguita tutte le volte che è necessario;
Il codice per il metodo [fillTableConstantes] è il seguente:
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()
]);
}
Commenti
- riga 1: il metodo [fillTableConstantes] riceve una connessione aperta come parametro. Sappiamo anche che all'interno di questa connessione è stata avviata una transazione;
- righe 5-6: la tabella [tbconstantes] viene azzerata;
- righe 9–31: preparazione dell'istruzione SQL INSERT. È complessa perché in questa operazione di inserimento ci sono 10 colonne da inizializzare e i nomi delle colonne devono essere recuperati dall'attributo [$database];
- righe 33–44: esecuzione dell'istruzione insert. C'è una sola riga da inserire. Anche in questo caso, il codice è reso complesso dalla necessità di recuperare i valori da inserire dall'attributo [$taxAdminData];
13.3.4. Lo script principale


Lo script principale si affida al livello [dao] per eseguire il trasferimento dei dati:
<?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;
Commenti
- righe 12–21: caricamento delle classi e delle interfacce dell'applicazione;
- righe 24–24: i due file JSON;
- riga 30: istanziamento del livello [DAO] passando i due file JSON al costruttore;
- riga 32: viene eseguito il trasferimento dei dati;
Quando eseguiamo questo codice, otteniamo il seguente risultato nel database:

La colonna [3] mostra i valori assegnati da MySQL alla chiave primaria [id]. La numerazione parte da 1. Lo screenshot qui sopra è stato catturato dopo aver eseguito lo script diverse volte.


13.4. Calcolo delle imposte

13.4.1. Architettura
La versione 04 dell'applicazione per il calcolo delle imposte utilizzava un'architettura a livelli:

Il livello [dao] implementa un'interfaccia [InterfaceDao]. Abbiamo creato una classe che implementa questa interfaccia:
- [DaoImpotsWithTaxAdminDataInJsonFile] che recuperava i dati fiscali da un file JSON. Quella era la versione 04;
Implementeremo l'interfaccia [InterfaceDao] utilizzando una nuova classe [DaoImpotsWithTaxAdminDataInDatabase] che recupererà i dati dell'amministrazione fiscale da un database MySQL. Il livello [dao], come in precedenza, scriverà i risultati e gli errori in file di testo e recupererà i dati dei contribuenti da un file di testo . Solo che questa volta, questi file di testo saranno file JSON. Inoltre, sappiamo che se continuiamo ad aderire all'interfaccia [InterfaceDao], il livello [business] non dovrà essere modificato.

13.4.2. L'entità [TaxPayerData]

La classe [TaxPayerData] viene utilizzata per incapsulare i dati del seguente file JSON [taxpayersdata.json] in una 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
}
]
La classe [TaxPayerData] è la seguente:
<?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
…
}
Commenti
- riga 7: la classe [TaxPayerData] estende la classe [BaseEntity]. Poiché i metodi della sua classe padre sono sufficienti, la classe [TaxPayerData] non ne definisce di propri. Si noti che gli attributi della classe [TaxPayerData] sono identici a quelli nel file JSON [taxpayersdata.json];
13.4.3. Il livello [dao]
13.4.3.1. Il tratto [TraitDao]
Il trait [TraitDao] implementa parte dell'interfaccia [InterfaceDao]. Rivediamolo:
<?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;
}
Il trait [TraitDao] implementa i metodi [getTaxPayersData] e [saveResults] dell'interfaccia [InterfaceDao]. Poiché la definizione dell'entità [TaxPayerData] è stata modificata tra le versioni 04 e 05, è necessario aggiornare il codice in [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]");
}
}
}
Commenti
- [TraitDao] implementa i metodi [getTaxPayersData] (riga 9) e [saveResults] (riga 86) dell'interfaccia [InterfaceDao];
- riga 9: il metodo [getTaxPayersData] accetta i seguenti parametri:
- [$taxPayersFilename]: il nome del file JSON contenente i dati dei contribuenti [taxpayersdata.json];
- [$errorsFilename]: il nome del file JSON contenente gli errori [errors.json];
- righe 11–13: il contenuto del file JSON contenente i dati dei contribuenti viene trasferito in un array associativo [$arrayOfAttributes]. Se il file JSON risulta inutilizzabile, viene generata un'eccezione [ExceptionImpots];
- riga 15: l'array [$taxPayersData] conterrà i dati dei contribuenti incapsulati in oggetti di tipo [TaxPayerData];
- riga 17: gli errori vengono accumulati nell'array [$errors];
- righe 99–33: costruzione dell'array [$taxPayersData];
- riga 22: Prima di essere incapsulati nel tipo [TaxPayerData], i dati vengono verificati. Il metodo [check] restituisce:
- un array [‘errors’=>[…]] contenente gli attributi errati se i dati non sono corretti;
- un array vuoto se i dati sono corretti;
- Riga 25: Quando i dati sono validi. Viene creato un nuovo oggetto [TaxPayerData] e aggiunto all’array [$taxPayersData];
- righe 26–30: caso in cui i dati non siano validi. Il record di errore include l'ID dell'oggetto [TaxPayerData] errato nel file JSON in modo che l'utente possa individuarlo, quindi l'errore viene aggiunto all'array [$errors];
- righe 35–39: gli errori riscontrati vengono registrati nel file JSON [$errorsFilename] passato come parametro alla riga 9;
- riga 41: viene restituito l'array di oggetti [TaxPayerData] costruiti: questo era l'obiettivo del metodo;
- righe 44–83: il metodo privato [check] verifica la validità dei parametri [married, children, salary] dell'array [$attributesOfTaxPayerData] passato come parametro alla riga 44. Se sono presenti attributi non validi, li raccoglie nell'array [$attributes] (righe 47, 53, 60, 70) sotto forma di un array [‘attributo non valido’=> valore dell'attributo non valido];
- riga 78: se ci sono errori, restituisce un array [‘errors’=>$attributes];
- riga 81: se non ci sono errori, viene restituito un array vuoto di errori;
- righe 86–93: implementazione del metodo [saveResults] dell'interfaccia [InterfaceDao];
- riga 90: costruiamo la stringa JSON da salvare nel file JSON [$resultsFilename] passato come parametro alla riga 86. Dobbiamo costruire la stringa JSON da un array:
- ogni elemento dell'array è separato dal successivo da una virgola e da un carattere di nuova riga;
- l'intero array è racchiuso tra parentesi quadre [];
- riga 92: la stringa JSON viene salvata nel file JSON [$resultsFilename];
13.4.3.2. La classe [DaoImpotsWithTaxAdminDataInDatabase]
La classe [DaoImpotsWithTaxAdminDataInDatabase] implementa l'interfaccia [InterfaceDao] come segue:
<?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;
}
}
Commenti
- riga 4: manteniamo lo spazio dei nomi già utilizzato per le altre implementazioni del livello [dao];
- riga 7: la classe [DaoImportsWithTaxAdminDataInDatabase] implementa l'interfaccia [InterfaceDao];
- riga 9: importiamo il trait [TraitDao]. Sappiamo che questo trait implementa parte dell'interfaccia. L'unico metodo rimasto da implementare è il metodo [getTaxAdminData] alle righe 62–64. Questo metodo restituisce semplicemente l'attributo privato [taxAdminData] della riga 11. Possiamo dedurre che il costruttore debba inizializzare questo attributo. Questo è il suo unico ruolo;
- Riga 16: il costruttore riceve un unico parametro, [$databaseFilename], che è il nome del file JSON [database.json] che definisce il database MySQL [dbimpots-2019];
- riga 18: il file JSON [$databaseFilename] viene utilizzato per creare un oggetto [Database], che viene costruito e memorizzato nell'attributo [$database] della riga 13. Se il file JSON non può essere elaborato correttamente, viene generata un'eccezione [ExceptionImpots];
- riga 20: viene creato l'oggetto [$this→taxAdminData], che il costruttore deve inizializzare;
- righe 22–26: viene aperta la connessione al database. Si noti la notazione [\PDO] per fare riferimento alla classe PHP [PDO]. Poiché ci troviamo nello spazio dei nomi [Application], se scrivessimo semplicemente [PDO], a questo nome relativo verrebbe anteposto lo spazio dei nomi corrente, risultando nella classe [Application\PDO], che non esiste;
- riga 28: se si verifica un errore, il DBMS genererà un'eccezione \PDOException (riga 37);
- riga 30: iniziamo una transazione. Questo non è realmente necessario poiché verranno eseguite solo due istruzioni SQL e queste istruzioni non modificano il database. Lo facciamo, tuttavia, per isolarci dagli altri utenti del database;
- riga 32: la tabella delle fasce di imposta [tbtranches] viene letta utilizzando il metodo privato [getTranches] della riga 52;
- riga 34: la tabella delle costanti di calcolo [tbconstantes] viene letta utilizzando il metodo privato [getConstantes] della riga 57;
- riga 36: se arriviamo a questa riga, significa che tutto è andato bene. Eseguiamo quindi il commit della transazione;
- righe 37–42: se arriviamo a questo punto, significa che si è verificata un'eccezione. Eseguiamo quindi il rollback della transazione se ne era in corso una (righe 39–42). Riga 44: per garantire la coerenza delle eccezioni, rilanciamo il messaggio di eccezione ricevuto, questa volta come eccezione di tipo [ExceptionImpots];
- righe 45–48: in tutti i casi (che si sia verificata o meno un'eccezione), chiudiamo la connessione;
Il metodo [getTranches] è il seguente:
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
]);
}
Commenti
- riga 1: il metodo riceve [$connexion] come parametro, che è una connessione aperta con una transazione in corso;
- righe 2–4: vengono create due scorciatoie per evitare di dover scrivere [$this->database] e [$taxAdminData = $this->taxAdminData] in tutto il codice. Si tratta di copie dei riferimenti agli oggetti, non di copie degli oggetti stessi;
- righe 6–10: viene preparata l'istruzione SELECT, che viene poi eseguita alla riga 12;
- righe 13–22: il risultato del SELECT viene elaborato. Le informazioni ricevute vengono memorizzate in tre array [limits, coeffR, coeffN];
- righe 24–28: i tre array vengono utilizzati per inizializzare l'attributo [$this->taxAdminData] della classe;
Il metodo privato [getConstantes] è il seguente:
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()});
}
Commenti
- riga 1: il metodo riceve [$connection] come parametro, che è una connessione aperta con una transazione in corso;
- righe 2–4: vengono create due scorciatoie per evitare di dover scrivere [$this->database] e [$taxAdminData = $this->taxAdminData] in tutto il codice. Si tratta di copie dei riferimenti agli oggetti, non di copie degli oggetti stessi;
- righe 6–15: viene preparata l'istruzione SELECT, che viene poi eseguita alla riga 15;
- righe 17–29: il risultato del SELECT viene elaborato. Le informazioni recuperate vengono utilizzate per inizializzare l'attributo [$this->taxAdminData] della classe;
Nota: si noti che la classe non dipende dal DBMS MySQL. È il codice chiamante che specifica il DBMS utilizzato tramite il DSN del database.
13.4.4. Il livello [business]

- Abbiamo appena implementato il livello [DAO] (3);
- Poiché abbiamo aderito all'interfaccia [InterfaceDao], il livello [business] (2) può teoricamente rimanere invariato. Tuttavia, non abbiamo modificato solo il livello [DAO]. Abbiamo modificato anche le entità, che sono condivise da tutti i livelli;
Il livello [business] implementa la seguente interfaccia [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;
}
- riga 12: il metodo [executeBatchImports] ora utilizza il file JSON [$taxPayersFileName], mentre nella versione 04 si trattava di un semplice file di testo. ;
Nella versione 04, il metodo [executeBatchImpots] era il seguente:
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);
}
- La riga 15 ora non è corretta. Nella nuova definizione della classe [TaxPayerData], il metodo [setMontant] non esiste più;
Nella versione 05, il metodo [executeBatchImpots] sarà il seguente:
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);
}
Commenti
- riga 15: invece di utilizzare i singoli setter della classe [TaxPayerData], utilizziamo il suo setter globale [setFromArrayOfAttributes];
- il resto del codice non deve essere modificato;
13.4.5. Lo script principale

- Abbiamo appena implementato i livelli [DAO] (3) e [logica di business] (2);
- dobbiamo ancora scrivere lo script principale (1);
Lo script principale è simile a quello della versione 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;
Commenti
- righe 12–22: caricamento di tutti i file della versione 05;
- righe 25–29: i nomi dei vari file JSON dell'applicazione;
- riga 33: costruzione del livello [DAO];
- riga 35: costruzione del livello [business];
- riga 37: chiamata al metodo [executeBatchImports] del livello [business];
Risultati
L'applicazione genera due file JSON:
- [results.json]: i risultati dei vari calcoli fiscali;
- [errors.json]: che riporta gli errori rilevati nel file JSON [taxpayersdata.json];
Il file [errors.json] è il seguente:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Ciò significa che nel file [taxpayersdata.json], la prima voce della tabella taxpayers è errata. Il file [taxpayersdata.json] era il seguente:
[
{
"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
}
]
Il file dei risultati [results.json] è il seguente:
[
{
"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
}
]
Questi risultati sono coerenti con quelli della versione 04.
13.5. Test [Codeception]
Come fatto nella sezione collegata alla versione 04, scriveremo i test [Codeception] per la versione 05.

13.5.1. Test del livello [dao]
Il test [DaoTest.php] è il seguente:
<?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());
…
}
}
Commenti
- righe 9–33: definizione dell'ambiente di test. Utilizziamo lo stesso dello script principale [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] descritto nella sezione collegata;
- righe 39–44: costruzione del livello [dao];
- riga 43: l'attributo [$this→taxAdminData] contiene i dati da testare;
- righe 47–51: il metodo [testTaxAdminData] è quello descritto nella sezione collegata;
I risultati del test sono i seguenti:

13.5.2. Test del livello [business]
Il test [MetierTest.php] è il seguente:
<?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"]);
}
}
Commenti
- righe 9–33: definizione dell'ambiente di test. Utilizziamo lo stesso dello script principale [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] descritto nella sezione collegata;
- righe 39–45: costruzione dei livelli [dao] e [business];
- riga 44: l'attributo [$this→business] fa riferimento al livello [business];
- righe 47–64: i metodi [test1, test2…, test11] sono quelli descritti nella sezione collegata;
I risultati del test sono i seguenti:
