13. Ejercicio práctico – version 5

Ya hemos escrito varias versiones de este ejercicio. La última, version, utilizaba una arquitectura por capas:

La capa [dao] implementa una interfaz [InterfaceDao]. Hemos creado una clase que implementa esta interfaz:
- [DaoImpotsWithTaxAdminDataInJsonFile], que recuperaba los datos fiscales de un archivo jSON;
Vamos a implementar la interfaz [InterfaceDao] mediante una nueva clase [DaoImpotsWithTaxAdminDataInDatabase] que recuperará los datos de la administración tributaria de una base de datos MySQL.
13.1. Creación de la base de datos [dbimpots-2019]
Siguiendo el ejemplo del párrafo anterior, creamos una base de datos MySQL denominada [dbimpots-2019], cuyo propietario será [admimpots] con la contraseña [mdpimpots]:

- En [1-4], arriba, vemos la base [dbimpots-2019], que por el momento no tiene tablas;

- en [1-5] arriba, vemos que el usuario [admimpots] tiene todos los derechos sobre la base de datos [dbimpots-2019]. Lo que no vemos aquí es que este usuario tiene la contraseña [admimpots];
Ahora creamos la tabla [tbtranches], que contendrá los tramos impositivos:

- En [1-7], creamos una tabla llamada [tbtranches] con 4 columnas;

- en [3-6] definimos una columna denominada [id] (3), de tipo entero [int] (4), que será la clave primaria [6] de la tabla y se autoincrementará [5] mediante SGBD. Esto significa que MySQL gestionará por sí mismo los valores de la clave primaria en el momento de las inserciones. Asignará el valor 1 a la clave primaria de la primera inserción, luego el 2 a la siguiente, etc.
- En [7], el asistente nos ofrece otras opciones de configuración de la clave primaria. Aquí nos limitamos a aceptar los valores predeterminados;

- en [8-16], definimos las otras tres columnas de la tabla:
- [limites] (8) de tipo número decimal (9) de 10 dígitos, 2 de ellos decimales (10), contendrá los elementos de la columna 17 de los tramos impositivos;
- [coeffR] (11) de tipo número decimal (12) de 6 cifras, 2 de ellas decimales (13), contendrá los elementos de la columna 18 de los tramos impositivos;
- [coeffN] (14), de tipo número decimal (15) de 10 dígitos, 2 de los cuales son decimales (16), contendrá los elementos de la columna 19 de los tramos impositivos;
Tras validar esta estructura, obtenemos el siguiente resultado:

- en [5], el icono de la clave indica que la columna [id] es la clave principal. También vemos que esta clave principal tiene valores enteros (6) y que es gestionada (autoincrementada) por MySQL;
Del mismo modo que hemos creado la tabla [tbtranches], creamos la tabla [tbconstantes], que contendrá las constantes del cálculo del impuesto:

Es posible exportar la estructura de la base de datos a un archivo de texto en forma de una secuencia de órdenes SQL:

El option [5] solo exporta aquí la estructura de la base de datos y no su contenido. En nuestro caso, la base aún no tiene contenido.



El option [11] genera el siguiente archivo SQL [dbimpots-2019.sql]:
-- phpMyAdmin SQL Volcado
-- version 4.8.5
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Hora de generación: 30 de junio de 2019 a las 01:10 PM
-- Servidor 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 */;
--
-- Base de datos: `dbimpots-2019`
--
CREATE DATABASE IF NOT EXISTS `dbimpots-2019` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
USE `dbimpots-2019`;
-- --------------------------------------------------------
--
-- Estructura de la tabla `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;
-- --------------------------------------------------------
--
-- Estructura de la tabla `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 de las tablas volcadas
--
--
-- Índices de la tabla `tbconstantes`
--
ALTER TABLE `tbconstantes`
ADD PRIMARY KEY (`id`);
--
-- Índices para la tabla `tbtranches`
--
ALTER TABLE `tbtranches`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT para tablas volcadas
--
--
-- AUTO_INCREMENT para la tabla `tbconstantes`
--
ALTER TABLE `tbconstantes`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT para la tabla `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 */;
Puede utilizar este archivo SQL para regenerar la base de datos [dbimpots-2019] si se ha destruido o alterado. No es necesario eliminar la base de datos antes de regenerarla, ya que el script SQL se encarga de hacerlo por sí mismo:


13.2. Organización del código
Para mostrar mejor la función de los diferentes scripts PHP que estamos escribiendo, vamos a organizar nuestro código en carpetas:

- en [1], visión general de version 05;
- en [2], las entidades de la aplicación, entidades intercambiadas entre capas;
- en [3], las utilidades de la aplicación;
- en [4], los datos utilizados o generados por la aplicación. Aquí tomamos la decisión de utilizar únicamente archivos jSON para los archivos de texto. Estos presentan varias ventajas:
- son reconocidos por muchas herramientas;
- estas herramientas cuentan con resaltado de sintaxis. Además, la notación jSON tiene unas reglas. Cuando estas no se respetan, las herramientas lo señalan. Por ejemplo, un error difícil de detectar en un archivo de texto básico es el uso de la letra O mayúscula o minúscula en lugar de ceros. Si se produce este error, se señalará. De hecho, en el código jSON:
«plafondRevenusCouplePourReduction»: 42O74
donde se ha puesto por descuido una O mayúscula en lugar del cero en [42074], Netbeans señala el error:

De hecho, Netbeans reconoce la O mayúscula que convierte a [49O74] en una cadena de caracteres. De ello deduce que la sintaxis debería ser [4-5]: la cadena [47O74] debería estar entre comillas. Así, se llama la atención del desarrollador sobre el error y este puede corregirlo: bien poniendo las comillas, bien sustituyendo la O por un cero;
Los demás elementos de version 05 son los siguientes:

- en [6], las interfaces y clases de la capa [Dao];
- en [7], las interfaces y clases de la capa [métier];
- en [8], los scripts principales de version 05;
El version 05 tiene dos objetivos distintos:
- rellenar la base MySQL [dbimpots-2019] con el contenido del archivo jSON [Data/txadmindata.json];
- implementar el cálculo del impuesto con datos fiscales que ahora proceden de la base MySQL [dbimpots-2019];
Vamos a tratar estos dos objetivos por separado.
13.3. Relleno de la base de datos [dbimpots-2019]
13.3.1. Objetivo
El archivo de texto taxadmindata.json contiene los datos de la administración tributaria:
{
"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
}
Nuestro objetivo es transferir estos datos a la base de datos MySQL [dbimpots-2019] creada anteriormente.
13.3.2. Las entidades

La entidad [Database] servirá para encapsular los datos del siguiente archivo 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 entidad [TaxAdminData] servirá para encapsular los datos del siguiente archivo 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 entidad [TaxPayerData] servirá para encapsular los datos del siguiente archivo 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 clase base [BaseEntity]
Para simplificar el código de las entidades, adoptaremos la siguiente regla: los atributos de una entidad tienen los mismos nombres que los atributos del archivo jSON que la entidad debe encapsular. Siguiendo esta regla, las entidades [Database, TaxAdminData, TaxPayerData] tienen puntos en común que pueden factorizarse en una clase padre. Será la siguiente clase [BaseEntity]:
<?php
namespace Application;
class BaseEntity {
// atributo
protected $arrayOfAttributes;
// inicialización a partir de un archivo jSON
public function setFromJsonFile(string $jsonFilename) {
// se recupera el contenido del archivo de datos fiscales
$fileContents = \file_get_contents($jsonFilename);
$erreur = FALSE;
// ¿error?
if (!$fileContents) {
// se registra el error
$erreur = TRUE;
$message = "Le fichier des données [$jsonFilename] n'existe pas";
}
if (!$erreur) {
// se recupera el código jSON del archivo de configuración en una tabla asociativa
$this->arrayOfAttributes = \json_decode($fileContents, true);
// ¿error?
if ($this->arrayOfAttributes === FALSE) {
// se registra el error
$erreur = TRUE;
$message = "Le fichier de données jSON [$jsonFilename] n'a pu être exploité correctement";
}
}
// ¿error?
if ($erreur) {
// se lanza una excepción
throw new ExceptionImpots($message);
}
// inicialización de los atributos de la clase
foreach ($this->arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// se devuelve el objeto
return $this;
}
public function checkForAllAttributes() {
// se comprueba que todas las claves se hayan inicializado
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) {
// se inicializan algunos atributos de la clase
foreach ($arrayOfAttributes as $key => $value) {
$this->$key = $value;
}
// se devuelve el objeto
return $this;
}
// toString
public function __toString() {
// atributos del objeto
$arrayOfAttributes = \get_object_vars($this);
// se elimina el atributo de la clase principal
unset($arrayOfAttributes["arrayOfAttributes"]);
// cadena Json del objeto
return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
}
// getter
public function getArrayOfAttributes() {
return $this->arrayOfAttributes;
}
}
Comentarios
- línea 5: la clase [BaseEntity] está destinada a ser extendida por las clases [Database, TaxAdminData, TaxPayerData];
- línea 7: el atributo [$arrayOfAttributes] es una matriz que contiene todos los atributos de la clase hija que ha extendido [BaseEntity], así como sus valores;
- líneas 9-41: el atributo [$arrayOfAttributes] se inicializa a partir del archivo jSON [$jsonFilename] pasado como parámetro. Se lanza una excepción de tipo [ExceptionImpot] si no se ha podido leer el archivo jSON o si no es un archivo jSON válido;
- líneas 36-38: aquí hay un código especial si lo ejecuta una clase hija. En este caso, [$this] representa una instancia de la clase hija [Database, TaxAdminData, TaxPayerData] y, en este caso, las líneas 36-38 inicializan los atributos de esta clase hija, siempre que dichos atributos tengan la visibilidad protected (o pública) (véase el párrafo enlace). De hecho, se ha dicho que los atributos de las entidades [Database, TaxAdminData, TaxPayerData] eran los mismos que los atributos del archivo jSON que encapsulaban. Por último, el método [setFromJsonFile] permite que una clase hija se inicialice a partir de un archivo jSON;
- línea 40: se devuelve el objeto [$this], es decir, una instancia de la clase hija, si el método [setFromJsonFile] ha sido llamado por una clase hija;
- líneas 43-51: el método [checkForAllAttributes] permite a una clase hija verificar que todos sus atributos se han inicializado. Si no es así, se lanza una excepción [ExceptionImpots]. Este método permite a la clase hija comprobar que su archivo jSON no ha omitido ciertos atributos;
- líneas 53-60: el método [setFromArrayOfAttributes] permite a una clase hija inicializar todos o parte de sus atributos a partir de una tabla asociativa cuyas claves tienen los mismos nombres que los atributos de la clase hija que se va a inicializar;
- líneas 63-70: el método [__toString] permite obtener la representación jSON de una clase hija;
13.3.2.2. La entidad [Database]
La entidad [Database] es la siguiente:
<?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;
…
}
La clase [Database] se utiliza para encapsular los datos del siguiente archivo 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 clase y el archivo jSON tienen los mismos atributos. Estos describen las características de la base de datos MySQL [dbimpots-2019]:
dsn | Nombre de la base de datos DSN |
id | Propietario de la base |
pwd | Su contraseña |
tableTranches | Nombre de la tabla que contiene los tramos impositivos |
colLimites colCoeffR colCoeffN | Nombres de las columnas de la tabla [tableTranches] |
tableConstantes | Nombre de la tabla que contiene las constantes de cálculo del impuesto |
colPlafondQfDemiPart colPlafondRevenusCelibatairePourReduction colPlafondRevenusCouplePourReduction colValeurReducDemiPart colPlafondDecoteCelibataire colPlafondDecoteCouple colPlafondImpotCelibatairePourDecote colPlafondImpotCouplePourDecote colAbattementDixPourcentMax colAbattementDixPourcentMin | Nombres de las columnas de la tabla [tableConstantes] que contiene las constantes de cálculo del impuesto |
¿Por qué nombrar las tablas y las columnas si ya se conocen sus nombres y no es algo que vaya a cambiar? Después de SGBD MySQL, vamos a utilizar SGBD PostgreSQL para almacenar los datos de la administración tributaria. Sin embargo, los nombres de las columnas y tablas de Postgres no siguen las mismas reglas que MySQL. Nos veremos obligados a utilizar otros nombres. Esto también es válido para otros SGBD. Si queremos que el código sea portable entre SGBD, es preferible utilizar parámetros en lugar de los nombres fijos de las tablas y columnas.
Volvamos al código de la clase [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
// inicialización
public function setFromJsonFile(string $jsonFilename) {
// padre
parent::setFromJsonFile($jsonFilename);
// se comprueba que todos los atributos se hayan inicializado
parent::checkForAllAttributes();
// se devuelve el objeto
return $this;
}
// getters y setters
public function getDsn() {
return $this->dsn;
}
…
public function setDsn($dsn) {
$this->dsn = $dsn;
return $this;
}
…
}
Comentarios
- líneas 7-24: todos los atributos de la clase tienen visibilidad [protected]. Esta es la condición para que puedan modificarse desde la clase padre [BaseEntity] (véase el párrafo del enlace);
- líneas 28-35: el método [setFromJsonFile] permite inicializar los atributos de la clase [Database] a partir del contenido de un archivo jSON pasado como parámetro. Los atributos del archivo jSON y los de la clase [Database] deben ser idénticos. Si el archivo jSON no es válido, se lanza una excepción;
- línea 30: es la clase padre la que realiza la inicialización;
- línea 32: se solicita a la clase padre que compruebe que se han inicializado todos los atributos de la clase [Database]. Si no es así, se lanza una excepción;
- línea 34: se devuelve la instancia [Database] que acaba de ser inicializada;
- líneas 37 y siguientes: los getter y setter de los atributos de la clase;
13.3.2.3. La entidad [TaxAdminData]
La entidad [TaxAdminData] es la siguiente:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// tramos impositivos
protected $limites;
protected $coeffR;
protected $coeffN;
// constantes de cálculo del impuesto
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
…
}
La clase [TaxAdminData] se utiliza para encapsular los datos del siguiente archivo 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 clase y el archivo jSON tienen los mismos atributos. Estos representan los datos de la administración tributaria. El resto del código de la clase [TaxAdminData] es el siguiente:
<?php
namespace Application;
class TaxAdminData extends BaseEntity {
// tramos impositivos
protected $limites;
protected $coeffR;
protected $coeffN;
// constantes de cálculo del impuesto
protected $plafondQfDemiPart;
protected $plafondRevenusCelibatairePourReduction;
protected $plafondRevenusCouplePourReduction;
protected $valeurReducDemiPart;
protected $plafondDecoteCelibataire;
protected $plafondDecoteCouple;
protected $plafondImpotCouplePourDecote;
protected $plafondImpotCelibatairePourDecote;
protected $abattementDixPourcentMax;
protected $abattementDixPourcentMin;
// inicialización
public function setFromJsonFile(string $taxAdminDataFilename) {
// padre
parent::setFromJsonFile($taxAdminDataFilename);
// se comprueba que todos los atributos se hayan inicializado
parent::checkForAllAttributes();
// se comprueba que los valores de los atributos sean números reales >=0
foreach ($this as $key => $value) {
if ($key !== "arrayOfAttributes") {
// $value debe ser un número real >=0 o una matriz de números reales >=0
$result = $this->check($value);
// ¿Error?
if ($result->erreur) {
// se lanza una excepción
throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
} else {
// se anota el valor
$this->$key = $result->value;
}
}
}
// se devuelve el objeto
return $this;
}
protected function check($value): \stdClass {
// $value es una matriz de elementos de tipo string o un único elemento
if (!\is_array($value)) {
$tableau = [$value];
} else {
$tableau = $value;
}
// se transforma la matriz de cadenas en una matriz de números reales
$newTableau = [];
$result = new \stdClass();
// los elementos de la matriz deben ser números decimales positivos o nulos
$modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
for ($i = 0; $i < count($tableau); $i ++) {
if (preg_match($modèle, $tableau[$i])) {
// se coloca el número flotante en newTableau
$newTableau[] = (float) $tableau[$i];
} else {
// se anota el error
$result->erreur = TRUE;
// salimos
return $result;
}
}
// se devuelve el resultado
$result->erreur = FALSE;
if (!\is_array($value)) {
// un único valor
$result->value = $newTableau[0];
} else {
// una lista de valores
$result->value = $newTableau;
}
return $result;
}
// getters y setters
…
}
Comentarios
- línea 23: el método [setFromJsonFile] sirve para inicializar los atributos de la clase [TaxAdminData] a partir de un archivo jSON pasado como parámetro. Es necesario que los atributos del archivo jSON existan con el mismo nombre en la clase;
- línea 25: es la clase padre la que realiza esta tarea;
- línea 27: se solicita a la clase padre que compruebe que todos los atributos de la clase hija se han inicializado;
- líneas 29-42: se comprueba localmente que todos los atributos tengan un valor real positivo o nulo. Esta comprobación ya se ha comentado en el apartado enlace de version 03;
13.3.3. La capa [dao]
Ahora podemos escribir el código que transferirá los datos del archivo de texto [taxadmindata.json] a las tablas [tbtranches, tbconstantes] de la base de datos MySQL [dbimpots-2019]. Adoptaremos la siguiente arquitectura:


La capa [dao] implementará la siguiente interfaz [InterfaceDao4TransferAdminDataFromFile2Database]:
<?php
// espacio de nombres
namespace Application;
interface InterfaceDao4TransferAdminData2Database {
public function transferAdminData2Database(): void;
}
Comentarios
- línea 8: el método [transferAdminData2Database] tiene como función almacenar los datos de la administración tributaria en una base de datos;
La interfaz [InterfaceDao4TransferAdminData2Database] será implementada por la siguiente clase [DaoTransferAdminDataFromJsonFile2Database]:
<?php
// espacio de nombres
namespace Application;
// definición de una clase TransferAdminDataFromFile2DatabaseDao
class DaoTransferAdminDataFromJsonFile2Database implements InterfaceDao4TransferAdminData2Database {
// atributos de la base de datos de destino
private $database;
// Datos de la administración tributaria
private $taxAdminData;
// fabricante
public function __construct(string $databaseFilename, string $taxAdminDataFilename) {
// se guarda la configuración de la base de datos
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// se guardan los datos fiscales
$this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
}
// transfiere los datos de los tramos impositivos desde un archivo de texto
// a la base de datos
public function transferAdminData2Database(): void {
// se trabaja en la base
$database = $this->database;
try {
// se abre la conexión a la base de datos
$connexion = new \PDO($database->getDsn(), $database->getId(), $database->getPwd());
// se desea que, ante cada error de SGBD, se lance una excepción
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// iniciamos una transacción
$connexion->beginTransaction();
// se rellena la tabla de tramos impositivos
$this->fillTableTranches($connexion);
// se rellena la tabla de constantes
$this->fillTableConstantes($connexion);
// se finaliza la transacción con éxito
$connexion->commit();
} catch (\PDOException $ex) {
// ¿Hay alguna transacción en curso?
if (isset($connexion) && $connexion->inTransaction()) {
// se finaliza la transacción con un error
$connexion->rollBack();
}
// se devuelve la excepción al código llamante
throw new ExceptionImpots($ex->getMessage());
} finally {
// se cierra la conexión
$connexion = NULL;
}
}
// Relleno de la tabla de tramos impositivos
private function fillTableTranches($connexion): void {
…
}
// Relleno de la tabla de constantes
private function fillTableConstantes($connexion): void {
…
}
}
Comentarios
Aquí aplicamos lo que hemos aprendido en el capítulo sobre MySQL.
- línea 7: la clase [DaoTransferAdminDataFromJsonFile2Database] implementa la interfaz [InterfaceDao4TransferAdminData2Database];
- línea 9: el atributo [$database] es el objeto de tipo [Database] que encapsula los datos del archivo [database.json];
- línea 11: el atributo [$taxAdminData] es el objeto de tipo [TaxAdminData] que encapsula los datos del archivo [taxadmindata.json];
- líneas 14-19: el constructor recibe como parámetros los nombres de los archivos [database.json, taxadmindata.json];
- línea 16: inicialización del atributo [$database];
- línea 18: inicialización del atributo [$taxAdminData];
- línea 23: se implementa el único método de la interfaz [InterfaceDao4TransferAdminData2Database];
- líneas 26-38: se rellenan las tablas [tbtranches, tbconstantes] en dos pasos:
- línea 34: primero se rellena la tabla [tbtranches]. Esto se realiza dentro de una transacción (líneas 32, 38). El método [fillTableTranches] (línea 55) lanza una excepción en cuanto algo sale mal. En ese caso, la ejecución continúa con el catch / finally de las líneas 39-50;
- línea 36: se rellena la tabla [tbconstantes] de la misma manera utilizando el método [fillTableConstantes] (línea 60);
- líneas 39-47: caso en el que el código ha lanzado una excepción;
- líneas 41-44: si existe una transacción, se cancela;
- línea 46: se lanza una excepción de tipo [ExceptionImpots] con el mensaje de la excepción original, que puede ser de cualquier tipo;
- líneas 47-50: en la cláusula [finally], se cierra la conexión;
El código del método [fillTableTranches] es el siguiente:
private function fillTableTranches($connexion): void {
// acceso directo a la base de datos
$database = $this->database;
// datos a insertar en la base de datos
$limites = $this->taxAdminData->getLimites();
$coeffR = $this->taxAdminData->getCoeffR();
$coeffN = $this->taxAdminData->getCoeffN();
// se vacía la tabla por si hubiera algo en ella
$statement = $connexion->prepare("delete from " . $database->getTableTranches());
$statement->execute();
// se preparan las inserciones
$sqlInsert = "insert into {$database->getTableTranches()} "
. "({$database->getColLimites()}, {$database->getColCoeffR()},"
. " {$database->getColCoeffN()}) values (:limites, :coeffR, :coeffN)";
$statement = $connexion->prepare($sqlInsert);
// ejecutamos la orden preparada con los valores de los tramos impositivos
for ($i = 0; $i < count($limites); $i++) {
$statement->execute([
"limites" => $limites[$i],
"coeffR" => $coeffR[$i],
"coeffN" => $coeffN[$i]]);
}
}
Comentarios
- línea 1: el método [fillTableTranches] recibe como parámetro una conexión abierta. Además, se sabe que se ha iniciado una transacción dentro de esta conexión;
- líneas 5-7: los valores que se deben insertar en la tabla son proporcionados por el atributo [$taxAdminData];
- líneas 9-10: se elimina el contenido actual de la tabla [tbtranches];
- líneas 12-15: se prepara la inserción de filas en la tabla. Aquí se utilizan los nombres de las columnas proporcionados por el atributo [$database];
- líneas 17-22: se ejecuta tantas veces como sea necesario la instrucción de inserción preparada en las líneas 12-15;
El código del método [fillTableConstantes] es el siguiente:
private function fillTableConstantes($connexion): void {
// atajo
$database = $this->database;
// se vacía la tabla por si hubiera algo en ella
$statement = $connexion->prepare("delete from {$database->getTableConstantes()}");
$statement->execute();
// se prepara la inserción
$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);
// se ejecuta la orden 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()
]);
}
Comentarios
- línea 1: el método [fillTableConstantes] recibe como parámetro una conexión abierta. Además, se sabe que se ha iniciado una transacción dentro de esta conexión;
- líneas 5-6: se vacía la tabla [tbconstantes];
- líneas 9-31: preparación de la orden de inserción SQL. Es compleja debido a que hay 10 columnas que inicializar en esta operación de inserción y a que hay que buscar los nombres de las columnas en el atributo [$database];
- líneas 33-44: ejecución de la orden de inserción. Solo hay una línea que insertar. Una vez más, el código se complica porque hay que buscar los valores a insertar en el atributo [$taxAdminData];
13.3.4. El script principal


El script principal se basa en la capa [dao] para realizar la transferencia de datos:
<?php
// se respetan estrictamente los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// gestión de errores mediante PHP
// ini_set("display_errors", "0");
// Inclusión de interfaz y clases
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";
//
// definición de constantes
const DATABASE_CONFIG_FILENAME = "../Data/database.json";
const TAXADMINDATA_FILENAME = "../Data/taxadmindata.json";
//
try {
// creación de la capa [dao]
$dao = new DaoTransferAdminDataFromJsonFile2Database(DATABASE_CONFIG_FILENAME, TAXADMINDATA_FILENAME);
// transferencia de datos a la base de datos
$dao->transferAdminData2Database();
} catch (ExceptionImpots $ex) {
// se muestra el error
print "L'erreur suivante s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
Comentarios
- líneas 12-21: carga de las clases e interfaces de la aplicación;
- líneas 24-24: los dos archivos jSON;
- línea 30: se instancia la capa [dao] pasando al constructor los dos archivos jSON;
- línea 32: se realiza la transferencia de datos;
Cuando ejecutamos este código, obtenemos el siguiente resultado en la base de datos:

En la columna [3], se ven los valores asignados por MySQL a la clave primaria [id]. La numeración comienza en 1. La captura de pantalla anterior se obtuvo tras varias ejecuciones del script.


13.4. Cálculo del impuesto

13.4.1. Arquitectura
La version 04 de la aplicación de cálculo de impuestos utilizaba una arquitectura por capas:

La capa [dao] implementa una interfaz [InterfaceDao]. Creamos una clase que implementaba esta interfaz:
- [DaoImpotsWithTaxAdminDataInJsonFile] que recuperaba los datos fiscales de un archivo jSON. Se trataba de la version 04;
Vamos a implementar la interfaz [InterfaceDao] mediante una nueva clase [DaoImpotsWithTaxAdminDataInDatabase] que recuperará los datos de la administración tributaria de una base de datos MySQL. La capa [dao], al igual que antes, escribirá los resultados y los errores en archivos de texto y buscará los datos de los contribuyentes también en un archivo de texto. Solo que esta vez, estos archivos de texto serán archivos jSON. Por otra parte, sabemos que si seguimos respetando la interfaz [InterfaceDao], no será necesario modificar la capa [métier].

13.4.2. La entidad [TaxPayerData]

La clase [TaxPayerData] sirve para encapsular en una clase los datos del siguiente archivo jSON [taxpayersdata.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
}
]
La clase [TaxPayerData] es la siguiente:
<?php
// espacio de nombres
namespace Application;
// clase de datos
class TaxPayerData extends BaseEntity {
// datos necesarios para el cálculo del impuesto del contribuyente
protected $marié;
protected $enfants;
protected $salaire;
// resultados del cálculo del impuesto
protected $impôt;
protected $surcôte;
protected $décôte;
protected $réduction;
protected $taux;
// getters y setters
…
}
Comentarios
- línea 7: la clase [TaxPayerData] extiende la clase [BaseEntity]. Dado que los métodos de su clase padre son suficientes, la clase [TaxPayerData] no define ninguno propio. Recordamos que los atributos de la clase [TaxPayerData] son idénticos a los del archivo jSON [taxpayersdata.json];
13.4.3. La capa [dao]
13.4.3.1. El rasgo [TraitDao]
El rasgo [TraitDao] implementa una parte de la interfaz [InterfaceDao]. Recordemos esta última:
<?php
// espacio de nombres
namespace Application;
interface InterfaceDao {
// lectura de datos de contribuyentes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;
// lectura de datos de la administración tributaria (tramos impositivos)
public function getTaxAdminData(): TaxAdminData;
// registro de resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void;
}
El rasgo [TraitDao] implementa los métodos [getTaxPayersData, saveResults] de la interfaz [InterfaceDao]. Dado que entre las versiones 04 y 05 se ha cambiado la definición de la entidad [TaxPayerData], debemos revisar el código de [TraitDao]:
<?php
// espacio de nombres
namespace Application;
trait TraitDao {
// lectura de datos de contribuyentes
public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
// se recuperan los datos de los contribuyentes en una tabla
$baseEntity = new BaseEntity();
$baseEntity->setFromJsonFile($taxPayersFilename);
$arrayOfAttributes = $baseEntity->getArrayOfAttributes();
// tabla de datos de contribuyentes
$taxPayersData = [];
// tabla de errores
$errors = [];
// recorrido por la tabla de atributos de elementos de tipo [TaxPayerData]
$i = 0;
foreach ($arrayOfAttributes as $attributesOfTaxPayerData) {
// verificación
$error = $this->check($attributesOfTaxPayerData);
if (!$error) {
// un contribuyente de +
$taxPayersData[] = (new TaxPayerData())->setFrOmArrayOfAttributes($attributesOfTaxPayerData);
} else {
// un error de + - se anota el número del dato inválido
$error = ["numéro" => $i] + $error;
$errors[] = $error;
}
// siguiente
$i++;
}
// se guardan los errores en un archivo json
$string = "";
foreach ($errors as $error) {
$string .= \json_encode($error, JSON_UNESCAPED_UNICODE) . "\n";
}
$this->saveString($errorsFilename, $string);
// resultado de la función
return $taxPayersData;
}
private function check(array $attributesOfTaxPayerData): array {
// se comprueban los datos de [$taxPayerData]
// la lista de atributos erróneos
$attributes = [];
// el estado civil debe ser sí o no
$marié = trim(strtolower($attributesOfTaxPayerData["marié"]));
$erreur = ($marié !== "oui" and $marié !== "non");
if ($erreur) {
// se anota el error
$attributes[] = ["marié" => $marié];
}
// el número de hijos debe ser un número entero positivo o cero
$enfants = trim($attributesOfTaxPayerData["enfants"]);
if (!preg_match("/^\d+$/", $enfants)) {
// se anota el error
$erreur = TRUE;
$attributes[] = ["enfants" => $enfants];
} else {
$enfants = (int) $enfants;
}
// el salario debe ser un número entero positivo o cero (sin céntimos de euro)
$salaire = trim($attributesOfTaxPayerData["salaire"]);
if (!preg_match("/^\d+$/", $salaire)) {
// se señala el error
$erreur = TRUE;
$attributes[] = ["salaire" => $salaire];
} else {
$salaire = (int) $salaire;
}
// ¿error?
if ($erreur) {
// respuesta con error
return ["erreurs" => $attributes];
} else {
// respuesta sin error
return [];
}
}
// registro de los resultados
public function saveResults(string $resultsFilename, array $taxPayersData): void {
// guardado de la tabla [$taxPayersData] en el archivo de texto [$resultsFileName]
// si el archivo de texto [$resultsFileName] no existe, se crea
// construcción de la cadena jSON a partir de los resultados
$string = "[" . implode(",
", $taxPayersData) . "]";
// guardado de esta cadena
$this->saveString($resultsFilename, $string);
}
// registro de los resultados de una tabla en un archivo de texto
private function saveString(string $fileName, string $data): void {
// guardar la cadena [$data] en el archivo de texto [$fileName]
// si el archivo de texto [$fileName] no existe, se crea
if (file_put_contents($fileName, $data) === FALSE) {
throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
}
}
}
Comentarios
- [TraitDao] implementa los métodos [getTaxPayersData] (línea 9) y [saveResults] (línea 86) de la interfaz [InterfaceDao];
- línea 9: el método [getTaxPayersData] recibe como parámetros:
- [$taxPayersFilename]: el nombre del archivo jSON de los datos de los contribuyentes [taxpayersdata.json];
- [$errorsFilename]: el nombre del archivo jSON de los errores [errors.json];
- líneas 11-13: el contenido del archivo jSON con los datos de los contribuyentes se transfiere a una tabla asociativa [$arrayOfAttributes]. Si el archivo jSON resulta inutilizable, se ha lanzado una excepción [ExceptionImpots];
- línea 15: la tabla [$taxPayersData] contendrá los datos de los contribuyentes encapsulados en objetos de tipo [TaxPayerData];
- línea 17: se acumularán los errores en la tabla [$errors];
- líneas 99-33: construcción de la matriz [$taxPayersData];
- línea 22: antes de encapsularse en un tipo [TaxPayerData], se verifican los datos. El método [check] devuelve:
- una matriz [‘erreurs’=>[…]] con los atributos erróneos si los datos son incorrectos;
- una matriz vacía si los datos son correctos;
- línea 25: caso en el que los datos son válidos. Se crea un nuevo objeto [TaxPayerData] y se añade a la matriz [$taxPayersData];
- líneas 26-30: caso en el que los datos no son válidos. En el error se anota el n.º del objeto [TaxPayerData] erróneo en el archivo jSON para que el usuario pueda localizarlo; a continuación, el error se añade a la tabla [$errors];
- líneas 35-39: se registran los errores encontrados en el archivo jSON [$errorsFilename] pasado como parámetro, línea 9;
- línea 41: se devuelve la matriz de objetos [TaxPayerData] construidos: ese era el objetivo del método;
- líneas 44-83: el método privado [check] comprueba la validez de los parámetros [marié, enfants, salaire] de la matriz [$attributesOfTaxPayerData] pasada como parámetro en la línea 44. Si hay atributos erróneos, los acumula en la tabla [$attributes] (líneas 47, 53, 60, 70) en forma de una tabla [‘attribut erroné’=> valeur de l’attribut erroné];
- línea 78: si hay errores, se devuelve una tabla [‘erreurs’=>$attributes];
- línea 81: si no hay errores, se devuelve una tabla de errores vacía;
- líneas 86-93: implementación del método [saveResults] de la interfaz [InterfaceDao];
- línea 90: se construye la cadena jSON para guardarla en el archivo jSON [$resultsFilename] pasado como parámetro en la línea 86. Se debe construir la cadena jSON a partir de una matriz:
- cada elemento de la matriz está separado del siguiente por una coma y un salto de línea;
- toda la matriz está entre corchetes [];
- línea 92: la cadena jSON se guarda en el archivo jSON [$resultsFilename];
13.4.3.2. La clase [DaoImpotsWithTaxAdminDataInDatabase]
La clase [DaoImpotsWithTaxAdminDataInDatabase] implementa la interfaz [InterfaceDao] de la siguiente manera:
<?php
// espacio de nombres
namespace Application;
// definición de una clase ImpotsWithDataInDatabase
class DaoImpotsWithTaxAdminDataInDatabase implements InterfaceDao {
// uso de una propiedad
use TraitDao;
// el objeto de tipo TaxAdminData que contiene los datos de los tramos impositivos
private $taxAdminData;
// el objeto de tipo [Database] que contiene las características de BD
private $database;
// constructor
public function __construct(string $databaseFilename) {
// se guarda la configuración jSON de la base de datos
$this->database = (new Database())->setFromJsonFile($databaseFilename);
// se prepara el atributo
$this->taxAdminData = new TaxAdminData();
try {
// se abre la conexión a la base de datos
$connexion = new \PDO(
$this->database->getDsn(),
$this->database->getId(),
$this->database->getPwd());
// se desea que, ante cada error de SGBD, se lance una excepción
$connexion->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
// se inicia una transacción
$connexion->beginTransaction();
// se rellena la tabla de tramos impositivos
$this->getTranches($connexion);
// se rellena la tabla de constantes
$this->getConstantes($connexion);
// se finaliza la transacción con éxito
$connexion->commit();
} catch (\PDOException $ex) {
// ¿Hay alguna transacción en curso?
if (isset($connexion) && $connexion->inTransaction()) {
// se finaliza la transacción con un error
$connexion->rollBack();
}
// se devuelve la excepción al código llamante
throw new ExceptionImpots($ex->getMessage());
} finally {
// se cierra la conexión
$connexion = NULL;
}
}
// lectura de los datos de la base
private function getTranches($connexion): void {
…
}
// lectura de la tabla de constantes
private function getConstantes($connexion): void {
…
}
// devuelve los datos que permiten calcular el impuesto
public function getTaxAdminData(): TaxAdminData {
return $this->taxAdminData;
}
}
Comentarios
- línea 4: se mantiene el espacio de nombres ya utilizado para las demás implementaciones de la capa [dao];
- línea 7: la clase [DaoImpotsWithTaxAdminDataInDatabase] implementa la interfaz [InterfaceDao];
- línea 9: se importa el rasgo [TraitDao]. Sabemos que este rasgo implementa una parte de la interfaz. El único método que queda por implementar es el método [getTaxAdminData] de las líneas 62-64. Este método se limita a devolver el atributo privado [taxAdminData] de la línea 11. De ello se deduce que el constructor deberá inicializar este atributo. Esa es su única función;
- línea 16: el constructor recibe como único parámetro [$databaseFilename], que es el nombre del archivo jSON [database.json] que define la base de datos MySQL [dbimpots-2019] ;
- línea 18: el archivo jSON [$databaseFilename] se utiliza para crear un objeto de tipo [Database], construido y almacenado en el atributo [$database] de la línea 13. Si el archivo jSON no se ha podido procesar correctamente, se ha lanzado una excepción [ExceptionImpots];
- línea 20: se crea el objeto [$this→taxAdminData] que el constructor debe inicializar;
- líneas 22-26: se abre la conexión a la base de datos. Obsérvese la notación [\PDO] para designar la clase [PDO] de PHP. De hecho, como nos encontramos en el espacio de nombres [Application], si escribiéramos simplemente [PDO], este nombre relativo llevaría como prefijo el espacio de nombres actual y daría lugar a la clase [Application\PDO], que no existe;
- línea 28: en caso de error, SGBD lanzará un \PDOException (línea 37);
- línea 30: se inicia una transacción. Esta no es realmente útil, ya que solo se ejecutarán dos órdenes SQL, órdenes que no modifican la base de datos. No obstante, se hace para aislarse de los demás usuarios de la base de datos;
- línea 32: la lectura de la tabla de tramos impositivos [tbtranches] se realiza mediante el método privado [getTranches] de la línea 52;
- línea 34: la lectura de la tabla de constantes de cálculo [tbconstantes] se realiza mediante el método privado [getConstantes] de la línea 57;
- línea 36: si se llega a esta línea es que todo ha salido bien. Por lo tanto, se valida la transacción;
- líneas 37-42: si se llega hasta aquí es porque se ha producido una excepción. Por lo tanto, invalidamos la transacción si había alguna en curso (líneas 39-42). Línea 44: para obtener excepciones homogéneas, reenviamos el mensaje de la excepción recibida, esta vez en forma de una excepción de tipo [ExceptionImpots];
- líneas 45-48: en todos los casos (haya excepción o no) se cierra la conexión;
El método [getTranches] es el siguiente:
private function getTranches($connexion): void {
// atajos
$database = $this->database;
$taxAdminData = $this->taxAdminData;
// se prepara la consulta SELECT
$statement = $connexion->prepare(
"select {$database->getColLimites()}," .
" {$database->getColCoeffR()}," .
" {$database->getColCoeffN()}" .
" from {$database->getTableTranches()}");
// ejecución de la orden preparada con los valores de los tramos impositivos
$statement->execute();
// se evalúa el resultado
$limites = [];
$coeffR = [];
$coeffN = [];
// se rellenan las tres tablas
while ($tranche = $statement->fetch(\PDO::FETCH_OBJ)) {
$limites[] = (float) $tranche->{$database->getColLimites()};
$coeffR[] = (float) $tranche->{$database->getColCoeffR()};
$coeffN[] = (float) $tranche->{$database->getColCoeffN()};
}
// se almacenan los datos en el atributo [$taxAdminData] de la clase
$taxAdminData->setFromArrayOfAttributes([
"limites" => $limites,
"coeffR" => $coeffR,
"coeffN" => $coeffN
]);
}
Comentarios
- línea 1: el método recibe como parámetro [$connexion], que es una conexión abierta en la que hay una transacción en curso;
- líneas 2-4: se crean dos atajos para evitar tener que escribir [$this->database] y [$taxAdminData = $this->taxAdminData] en todo el código. Se trata de copias de referencias de objetos y no de una copia de los propios objetos;
- líneas 6-10: se prepara la orden SELECT, que se ejecuta en la línea 12;
- líneas 13-22: se procesa el resultado de SELECT. La información recibida se acumula en tres tablas [limites, coeffR, coeffN];
- líneas 24-28: las tres tablas se utilizan para inicializar el atributo [$this->taxAdminData] de la clase;
El método privado [getConstantes] es el siguiente:
private function getConstantes($connexion): void {
// atajos
$database = $this->database;
$taxAdminData = $this->taxAdminData;
// se prepara la 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);
// se ejecuta la orden preparada
$statement->execute();
// se procesa el resultado: aquí solo hay una línea
$row = $statement->fetch(\PDO::FETCH_OBJ);
// se inicializa el 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()});
}
Comentarios
- línea 1: el método recibe como parámetro [$connexion], que es una conexión abierta en la que hay una transacción en curso;
- líneas 2-4: se crean dos atajos para evitar tener que escribir [$this->database] y [$taxAdminData = $this->taxAdminData] en todo el código. Se trata de copias de referencias de objetos y no de una copia de los propios objetos;
- líneas 6-15: se prepara la orden SELECT, que se ejecuta en la línea 15;
- líneas 17-29: se procesa el resultado de SELECT. La información recuperada se utiliza para inicializar el atributo [$this->taxAdminData] de la clase;
Nota: cabe señalar que la clase no depende de SGBD ni de MySQL. Es el código llamante el que establece el SGBD utilizado a través del DSN de la base de datos.
13.4.4. La capa [métier]

- Acabamos de implementar la capa [dao] (3);
- dado que hemos respetado la interfaz [InterfaceDao], la capa [métier] (2) puede, en teoría, permanecer sin cambios. Sin embargo, no solo hemos modificado la capa [dao]. También hemos modificado las entidades que son compartidas por todas las capas;
La capa [métier] implementa la siguiente interfaz [InterfaceMetier]:
<?php
// espacio de nombres
namespace Application;
interface InterfaceMetier {
// cálculo de los impuestos de un contribuyente
public function calculerImpot(string $marié, int $enfants, int $salaire): array;
// cálculo de impuestos en modo batch
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}
- línea 12: el método [executeBatchImpots] utiliza ahora el archivo jSON [$taxPayersFileName], mientras que en la versión version 04 se trataba de un archivo de texto básico. ;
En version 04, el método [executeBatchImpots] era el siguiente:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// se permiten las excepciones que provienen de la capa [dao]
// se recuperan los datos de los contribuyentes
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabla de resultados
$results = [];
// se procesan
foreach ($taxPayersData as $taxPayerData) {
// se calcula el impuesto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// se completa [$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"]);
// se introduce el resultado en la tabla de resultados
$results [] = $taxPayerData;
}
// registro de los resultados
$this->dao->saveResults($resultsFileName, $results);
}
- la línea 15 ahora es errónea. En la nueva definición de la clase [TaxPayerData], el método [setMontant] ya no existe;
En version 05, el método [executeBatchImpots] será el siguiente:
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
// se dejan pasar las excepciones que provienen de la capa [dao]
// se recuperan los datos de los contribuyentes
$taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
// tabla de resultados
$results = [];
// se procesan
foreach ($taxPayersData as $taxPayerData) {
// se calcula el impuesto
$result = $this->calculerImpot(
$taxPayerData->getMarié(),
$taxPayerData->getEnfants(),
$taxPayerData->getSalaire());
// se completa [$taxPayerData]
$taxPayerData->setFromArrayOfAttributes($result);
// se introduce el resultado en la tabla de resultados
$results [] = $taxPayerData;
}
// registro de los resultados
$this->dao->saveResults($resultsFileName, $results);
}
Comentarios
- línea 15: en lugar de utilizar los setters individuales de la clase [TaxPayerData], se utiliza su setter global [setFromArrayOfAttributes];
- el resto del código no tiene que modificarse;
13.4.5. El script principal

- Acabamos de implementar las capas [dao] (3) y [métier] (2);
- nos queda por escribir el script principal (1);
El script principal es similar al de version 04:
<?php
// Se respetan estrictamente los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// gestión de errores mediante PHP
//ini_set("display_errors", "0");
// Inclusión de la interfaz y las clases
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";
//
// definición de 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 {
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// creación de la capa [métier]
$métier = new Metier($dao);
// cálculo de impuestos en modo batch
$métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
// se muestra el error
print "Une erreur s'est produite : " . utf8_encode($ex->getMessage()) . "\n";
}
// fin
print "Terminé\n";
exit;
Comentarios
- líneas 12-22: carga de todos los archivos de version 05;
- líneas 25-29: los nombres de los diferentes archivos jSON de la aplicación;
- línea 33: construcción de la capa [dao];
- línea 35: construcción de la capa [métier];
- línea 37: llamada al método [executeBatchImpots] de la capa [métier];
Resultados
La aplicación genera dos archivos jSON:
- [resultats.json]: los resultados de los distintos cálculos de impuestos;
- [errors.json]: que señala los errores encontrados en el archivo jSON [taxpayersdata.json];
El archivo [errors.json] es el siguiente:
{
"numéro": 1,
"erreurs": [
{
"marié": "ouix"
},
{
"enfants": "2x"
},
{
"salaire": "55555x"
}
]
}
Esto significa que en [taxpayersdata.json], el elemento n.º 1 de la tabla de contribuyentes es erróneo. El archivo [taxpayersdata.json] era el siguiente:
[
{
"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
}
]
El archivo de resultados [resultats.json] es el siguiente:
[
{
"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
}
]
Estos resultados coinciden con los de version 04.
13.5. Pruebas [Codeception]
Al igual que se hizo en el apartado del enlace para el version 04, vamos a escribir pruebas [Codeception] para el version 05.

13.5.1. Prueba de la capa [dao]
La prueba [DaoTest.php] es la siguiente:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// directorios raíz
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Inclusión de interfaces y clases
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 terceros
require_once VENDOR . "/autoload.php";
// definición 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();
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
$this->taxAdminData = $dao->getTaxAdminData();
}
// pruebas
public function testTaxAdminData() {
// constantes de cálculo
$this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
…
}
}
Comentarios
- líneas 9-33: definición del entorno de la prueba. Utilizamos el mismo que el utilizado por el script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] descrito en el párrafo enlace;
- líneas 39-44: construcción de la capa [dao];
- línea 43: el atributo [$this→taxAdminData] contiene los datos que se van a probar;
- líneas 47-51: el método [testTaxAdminData] es el descrito en el apartado enlace;
Los resultados de la prueba son los siguientes:

13.5.2. Prueba de la capa [métier]
La prueba [MetierTest.php] es la siguiente:
<?php
// Cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// espacio de nombres
namespace Application;
// directorios raíz
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-05");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// Inclusión de interfaces y clases
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 terceros
require_once VENDOR . "/autoload.php";
// definición 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 {
// capa de negocio
private $métier;
public function __construct() {
parent::__construct();
// creación de la capa [dao]
$dao = new DaoImpotsWithTaxAdminDataInDatabase(DATABASE_CONFIG_FILENAME);
// creación de la capa [métier]
$this->métier = new Metier($dao);
}
// pruebas
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"]);
}
}
Comentarios
- líneas 9-33: definición del entorno de la prueba. Utilizamos el mismo que el utilizado por el script principal [MainCalculateImpotsWithTaxAdminDataInMySQLDatabase] descrito en el párrafo enlace;
- líneas 39-45: construcción de las capas [dao] y [métier];
- línea 44: el atributo [$this→métier] hace referencia a la capa [métier];
- líneas 47-64: los métodos [test1, test2…, test11] son los descritos en el apartado enlace;
Los resultados de la prueba son los siguientes:
