IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction au langage PHP 7 par l'exemple


précédentsommairesuivant

XI. Exercice d’application – version 4

L’application de calcul d’impôts va implémenter la structure en couches suivante :

Image non disponible

Nous allons reprendre les éléments de la version 3 du paragraphe lienExercice d'application – version 3 en les modifiant pour les adapter à la nouvelle architecture de l’application. On appelle parfois cela du ‘refactoring’. Nous supposons ici que les données nécessaires à l’application sont dans des fichiers texte. C’est la couche [Dao] qui va s’occuper des échanges avec ces fichiers.

XI-A. Arborescence des scripts

Image non disponible

XI-B. Objets échangés entre couches

Nous allons garder certains objets de la version 3. Nous les redonnons ici pour rappel.

L’exception [ExceptionImpots] est l’exception que lancera la couche [Dao] lorsqu’elle rencontrera un problème soit avec l’accès aux données soit avec la nature des données (données incorrectes).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
<?php

// espace de noms
namespace Application;

class ExceptionImpots extends \RuntimeException {

  public function __construct(string $message, int $code=0) {
    parent::__construct($message, $code);
  }
}

La classe [Utilitaires] rassemble des méthodes utiles à la gestion des fichiers texte (ici une seule méthode) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<?php

// espace de noms
namespace Application;

// une classe de fonctions utilitaires
abstract class Utilitaires {

  public static function cutNewLinechar(string $ligne): string {
    // on supprime la marque de fin de ligne de $ligne si elle existe
    $longueur = strlen($ligne);  // longueur ligne
    while (substr($ligne, $longueur - 1, 1) == "\n" or substr($ligne, $longueur - 1, 1) == "\r") {
      $ligne = substr($ligne, 0, $longueur - 1);
      $longueur--;
    }
    // fin - on rend la ligne
    return($ligne);
  }
}

La classe [TaxAdminData] est la classe qui encapsule les données de l’administration fiscale :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
<?php

namespace Application;

class TaxAdminData {
  // tranches d'impôt
  private $limites;
  private $coeffR;
  private $coeffN;
  // constantes de calcul de l'impôt
  private $plafondQfDemiPart;
  private $plafondRevenusCelibatairePourReduction;
  private $plafondRevenusCouplePourReduction;
  private $valeurReducDemiPart;
  private $plafondDecoteCelibataire;
  private $plafondDecoteCouple;
  private $plafondImpotCouplePourDecote;
  private $plafondImpotCelibatairePourDecote;
  private $abattementDixPourcentMax;
  private $abattementDixPourcentMin;

  // initialisation
  public function setFromJsonFile(string $taxAdminDataFilename): TaxAdminData {
    // on récupère le contenu du fichier des données fiscales
    $fileContents = \file_get_contents($taxAdminDataFilename);// on rend l'objet
    return $this;
  }

  private function check($value): \stdClass {return $result;
  }

    // toString
  public function __toString() {
    // chaîne Json de l'objet
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

  // getters et setters
  public function getLimites() {
    return $this->limites;
  }public function setLimites($limites) {
    $this->limites = $limites;
    return $this;
  }}

Nous ajoutons une nouvelle classe [TaxPayerData] qui encapsule les données écrites dans le fichier des résultats :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
<?php

// espace de noms
namespace Application;

// la classe des données
class TaxPayerData {
  // données nécessaires au calcul de l'impôt du contribuable
  private $marié;
  private $enfants;
  private $salaire;
  // résultats du calcul de l'impôt
  private $montant;
  private $surcôte;
  private $décôte;
  private $réduction;
  private $taux;

  // setter
  public function setFromParameters(string $marié, int $nbEnfants, int $salaireAnnuel: TaxPayerData{
    // données du contribuable nécessaires au calcul de l'impôt
    $this->marié = $marié;
    $this->enfants = $nbEnfants;
    $this->salaire = $salaireAnnuel;
    // on rend l'objet initialisé
    return $this;
  }

  // getters et setters
  public function getMarié() {
    return $this->marié;
  }public function setMarié($marié) {
    $this->marié = $marié;
    return $this;
  }// toString
  public function __toString() {
    // chaîne Json de l'objet
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

}

Note : utilisez la génération automatique de code pour générer le constructeur, les getters et setters (cf paragraphe lienLa classe [TaxAdminData]). Remarquez que les setters sont ‘fluents’.

XI-C. La couche [dao]

Nous nous intéressons ici à la couche [1] de notre application :

Image non disponible

XI-C-1. L’interface [InterfaceDao]

L’interface de la couche [dao] sera la suivante [InterfaceDao.php] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?php

// espace de noms
namespace Application;

interface InterfaceDao {

  // lecture des données contribuables
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // lecture des données de l'administration fiscale (tranches d'impôts)
  public function getTaxAdminData(): TaxAdminData;

  // enregistrement des résultats
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Commentaires

  • le cahier des charges est ici le suivant :
    • les données des contribuables sont dans un fichier texte ;
    • on enregistre les résultats du calcul d’impôts dans un fichier texte ;
    • on enregistre les éventuelles erreurs dans un fichier texte ;
    • on ne sait pas sous quelle forme sont disponibles les données de l’administration fiscale. Pour chaque nouvelle forme, l’interface [InterfaceDao] devra être implémentée par une nouvelle classe ;
    • les méthodes de l’interface qui rencontrent une erreur irrécupérable lors de l’accès aux données doivent lancer une exception de type [ExceptionImpots] ;
  • ligne 9 : la méthode qui permet d’obtenir les données du contribuable [statut marital, nombre d’enfants, salaire annuel] ;
    • le 1er paramètre est le nom du fichier texte dans lequel se trouvent ces données ;
    • le second paramètre est le nom du fichier texte dans lequel enregistrer les éventuelles erreurs rencontrées ;
  • ligne 12 : la méthode qui permet d’obtenir les données de l’administration fiscale. On ne lui passe ici aucun paramètre car on ne sait pas comment elles sont stockées ;
  • ligne 15 : la méthode qui permet d’enregistrer les résultats du calcul de l’impôt dans un fichier texte dont on passe le nom en paramètre ;

Lorsqu’on écrit l’interface [InterfaceDao], on sait qu’il y aura différentes façons d’écrire la méthode [getTaxAdminData] selon la façon dont seront stockées les données de l’administration fiscale. L’interface [InterfaceDao] sera donc implémentée par différentes classes, chacune s’occupant d’un stockage particulier de ces données (tableaux, fichiers texte, base de données, service web). Ces classes dérivées auront néanmoins un code commun, celui de l’implémentation des méthodes [getTaxPayersData, saveResults]. On sait que ce cas d’utilisation peut être implémenté de deux façons (cf paragraphe lienUtiliser un trait à la place d’une classe abstraite):

  • on crée une classe abstraite C qui regroupe le code commun aux classes dérivées. La classe C implémente l’interface I mais certaines méthodes qui doivent être déclarées dans les classes dérivées sont dans la classe C déclarées abstraites et donc la classe C est elle-même abstraite. On crée ensuite des classes C1 et C2 dérivées de C qui implémentent chacune à leur manière les méthodes non définies (abstraites) de leur classe parent C ;
  • on crée un trait T quasi identique à la classe abstraite C de la solution précédente. Ce trait n’implémente pas l’interface I car syntaxiquement elle ne le peut pas. On crée ensuite des classes C1 et C2 implémentant l’interface I et utilisant le trait T. Il ne reste à ces classes qu’à implémenter les méthodes de l’interface I non implémentées par le trait T qu’elles importent ;

Pour l’exemple, nous allons utiliser ici un trait [TraitDao].

XI-C-2. Le trait [TraitDao]

Le code du trait [TraitDao] est le suivant [TraitDao.php] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
<?php

// espace de noms
namespace Application;

trait TraitDao {

  // lecture des données contribuables
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array {
    // tableau des données contribuables
    $taxPayersData = [];
    // tableau des erreurs
    $errors = [];
    // pas mal d'erreurs peuvent se produire dès qu'on gère des fichiers
    try {
      // lecture des données utilisateur
      // chaque ligne a la forme statut marital, nombre d'enfants, salaire annuel
      $taxPayersFile = fopen($taxPayersFilename, "r");
      if (!$taxPayersFile) {
        throw new ExceptionImpots("Impossible d'ouvrir en lecture les déclarations des contribuables [$taxPayersFilename]", 12);
      }
      // on exploite la ligne courante du fichier des données utilisateur
      // qui a la forme statut marital, nombre d'enfants, salaire annuel
      $num = 1;         // n° ligne courante
      $nbErreurs = 0;   // nbre d'erreurs rencontrées
      while ($ligne = fgets($taxPayersFile, 100)) {
        // on néglige les lignes vides
        $ligne = trim($ligne);
        if (strlen($ligne) == 0) {
          // ligne suivante
          $num++;
          // on reboucle
          continue;
        }
        // on enlève l'éventuelle marque de fin de ligne
        $ligne = Utilitaires::cutNewLineChar($ligne);
        // on récupère les 3 champs marié:enfants:salaire qui forment $ligne
        list($marié, $enfants, $salaire) = explode(",", $ligne);
        // on les vérifie
        // le statut marital doit être oui ou non
        $marié = trim(strtolower($marié));
        $erreur = ($marié !== "oui" and $marié !== "non");
        if (!$erreur) {
          // le nombre d'enfants doit être un entier
          $enfants = trim($enfants);
          if (!preg_match("/^\d+$/", $enfants)) {
            $erreur = TRUE;
          } else {
            $enfants = (int) $enfants;
          }
        }
        if (!$erreur) {
          // le salaire est un entier sans les centimes d'euros
          $salaire = trim($salaire);
          if (!preg_match("/^\d+$/", $salaire)) {
            $erreur = TRUE;
          } else {
            $salaire = (int) $salaire;
          }
        }
        // erreur ?
        if ($erreur) {
          $errors[] = "la ligne [$num] du fichier [$taxPayersFilename] est erronée";
          $nbErreurs++;
        } else {
          // on mémorise les informations
          $taxPayersData[] = (new TaxPayerData())->setFromParameters($marié, $enfants, $salaire);
        }
        // ligne suivante
        $num++;
      }
      // est-on à la fin du fichier ?
      if (!feof($taxPayersFile)) {
        // on est sorti de la boucle sur une erreur de lecture
        throw new ExceptionImpots("Erreur lors de la lecture de la ligne n° [$num] du fichier [$taxPayersFilename]");
      } else {
        // on est sorti de la boucle sur la marque de fin de fichier
        // on sauve les erreurs dans un fichier texte
        $this->saveString($errorsFilename, implode("\n", $errors));
        // résultat de la fonction
        return $taxPayersData;
      }
    } finally {
      // on ferme le fichier s'il est ouvert
      if ($taxPayersFile) {
        fclose($taxPayersFile);
      }
    }
  }

  // enregistrement des résultats
  public function saveResults(string $resultsFilename, array $taxPayersData): void {
    // enregistrement du tableau [$taxPayersData] dans le fichier texte [$resultsFileName]
    // si le fichier texte [$resultsFileName] n'existe pas, il est créé
    $this->saveString($resultsFilename, implode("\n", $taxPayersData));
  }

  // enregistrement d'es résultats d'un tableau dans un fichier texte
  private function saveString(string $fileName, string $data): void {
    // enregistrement du tableau [$data] dans le fichier texte [$fileName]
    // si le fichier texte [$fileName] n'existe pas, il est créé
    if (file_put_contents($fileName, $data) === FALSE) {
      throw new ExceptionImpots("Erreur lors de l'enregistrement de données dans le fichier texte [$fileName]");
    }
  }

}

Commentaires

  • ligne 6 : nous définissons ici un trait et non une classe ;
  • lignes 9-89 : la méthode [getTaxPayersData] implémente la méthode de même nom de l’interface [InterfaceDao]. Elle récupère dans un fichier texte nommé [$taxPayersFilename] les données des contribuables [statut marital, nombre d’enfants, salaire annuel]. Elle rend celles-ci sous la forme d’un tableau [$taxPayersData] d’éléments de type [TaxPayerData] (lignes 67, 81) ;
  • la méthode [getTaxPayersData] est très semblable à la méthode [AbstractBaseImpots::executeBatchImpots] décrite au paragraphe lienLa classe abstraite [AbstractBaseImpots] avec les différences suivantes :
    • la méthode [getTaxPayersData] ne fait que récupérer les données des contribuables. Elle ne fait pas de calcul d’impôt. Ici c’est le rôle de la couche [métier] ;
    • comme le faisait la méthode [executeBatchImpots] elle signale les erreurs. Ici les erreurs sont d’abord mémorisées dans un tableau [$errors] (ligne 13), tableau qui est mémorisé dans un fichier texte à la fin du traitement (ligne 79). Selon les cas, il est vide ou non ;
    • dans le cas d’erreur irrécupérable, une exception de type [ExceptionImpots] est lancée (lignes 20, 75) ;
  • ligne 73 : on notera le traitement fait à la sortie de la boucle des lignes 26-71. En effet la fonction [fgets] a l’inconvénient de rendre le booléen FALSE aussi bien lorsque la lecture des lignes a rencontré la marque de fin de fichier que si cette lecture n’a pu aboutir à cause d’une erreur. Pour différentier les deux cas, on teste si on est rendu à la fin du fichier avec la fonction [feof]. Si on n’est pas rendu à la fin du fichier, c’est qu’une erreur s’est produite et on lance alors une exception ;
  • lignes 83-88 : le [finally] est exécuté qu’il y ait eu exception ou pas lors de l’exploitation du fichier ;
  • ligne 85 : si le fichier a été ouvert, alors le ‘handle’ [$taxPayersFile] du fichier a la valeur booléenne TRUE, FALSE sinon ;
  • lignes 99-105 : la méthode privée [saveString] utilisée ligne 79 pour enregistrer le tableau des erreurs dans un fichier texte ;
  • ligne 99 : la méthode [saveString] reçoit deux paramètres :
    • [string $filename] qui est le nom du fichier texte utilisé pour enregistrer les données ;
    • [string $data] qui est la chaîne de caractères à enregistrer dans le fichier texte. Cette chaîne sera un ensemble de lignes terminée par le caractère de fin de ligne \n ;
  • ligne 102 : la fonction PHP [file_puts_contents] enregistre une chaîne de caractères dans un fichier texte. Elle s’occupe d’ouvrir le fichier, d’écrire la chaîne dedans et de fermer le fichier. Elle rend le booléen FALSE si une erreur s’est produite ;
  • ligne 103 : si une erreur se produit, on lance une exception ;
  • lignes 92-96 : implémentation de la méthode [saveResults] de l’interface [InterfaceDao]. On utilise de nouveau la méthode privée [saveString]. Ici le second paramètre de [saveString] est une chaîne construite à partir du tableau [$taxPayersData] dont les éléments sont de type [TaxPayerData]. On peut se demander quel va être le résultat de l’opération :
 
Sélectionnez
implode("\n", $taxPayersData)

Nous avons défini dans la classe [TaxPayerData] (paragraphe lienObjets échangés entre couches) la méthode [__toString] suivante :

 
Sélectionnez
public function __toString() {
    // chaîne Json de l'objet
    return \json_encode(\get_object_vars($this), JSON_UNESCAPED_UNICODE);
  }

L’opération

 
Sélectionnez
implode("\n", $taxPayersData)

va concaténer chaque élément du tableau [$taxPayersData] transformé en chaîne de caractères par sa méthode [__toString] avec la marque de fin de ligne \n. Cela va donner une chaîne de caractères de la forme :

json1\njson2\n

Conclusion

Le trait [TraitDao] a implémenté deux des méthodes de l’interface [InterfaceDao], [getTaxPayersData] et [saveResults] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<?php

// espace de noms
namespace Application;

interface InterfaceDao {

  // lecture des données contribuables
  public function getTaxPayersData(string $taxPayersFilename, string $errorsFilename): array;

  // lecture des données de l'administration fiscale (tranches d'impôts)
  public function getTaxAdminData(): TaxAdminData;

  // enregistrement des résultats
  public function saveResults(string $resultsFilename, array $taxPayersData): void;
}

Il nous reste à implémenter la méthode [getTaxAdminData] qui récupère les données de l’administration fiscale.

XI-C-3. La classe [ImpotsWithTaxAdminDataInJsonFile]

La classe [ImpotsWithTaxAdminDataInJsonFile] implémente l’interface [InterfaceDao] de la façon suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
<?php

// espace de noms
namespace Application;

// définition d'une classe ImpotsWithDataInFile
class DaoImpotsWithTaxAdminDataInJsonFile implements InterfaceDao {
  // usage d'un trait
  use TraitDao;
  // l'objet de type TaxAdminData qui contient les données des tranches d'impôts
  private $taxAdminData;

  // le constructeur
  public function __construct(string $taxAdminDataFilename) {
    // on veut initialiser l'attribut [$this->taxAdminData]
    $this->taxAdminData = (new TaxAdminData())->setFromJsonFile($taxAdminDataFilename);
  }

  // retourne les données permettant le calcul de l'impôt
  public function getTaxAdminData(): TaxAdminData {
    return $this->taxAdminData;
  }
}

Commentaires

  • ligne 7 : la classe [ImpotsWithTaxAdminDataInJsonFile] implémente l’interface [InterfaceDao] ;
  • ligne 9 : la classe [ImpotsWithTaxAdminDataInJsonFile] utilise le trait [traitDao] qui on le sait implémente les méthodes [getTaxPayersData] et [saveResults] de l’interface [InterfaceDao]. Il ne reste donc plus, à la classe [ImpotsWithTaxAdminDataInJsonFile] qu’à implémenter la méthode [getTaxAdminData] qui récupère les données de l’administration fiscale ;
  • ligne 11 : l’attribut de type [TaxAdminData] que rend la méthode [getTaxAdminData] des lignes 20-22. Cet attribut est initialisé par le constructeur des lignes 14-17 ;

Nous en avons terminé avec la couche [dao] de notre application : nous avons une classe qui implémente totalement l’interface [InterfaceDao] que nous nous sommes imposés. Nous pouvons désormais passer à la couche [métier].

XI-D. La couche [métier]

Nous allons maintenant implémenter la couche [2] de notre architecture :

Image non disponible

XI-D-1. L’interface [InterfaceMétier]

L’interface de la couche [métier] sera la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<?php

// espace de noms
namespace Application;

interface InterfaceMetier {

  // calcul des impôts d'un contribuable
  public function calculerImpot(string $marié, int $enfants, int $salaire): array;

  // calcul des impôts en mode batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void;
}

Commentaires

  • ligne 9 : l’interface [InterfaceMétier] sait calculer le montant de l’impôt d’un contribuable particulier pourvu qu’on lui donne les informations suivantes : statut marital, nombre d’enfants, salaire annuel. La méthode [calculerImpot] n’utilise pas la couche [dao] aussi ne lance-t-elle pas d’exceptions ;
  • ligne 9 : l’interface [InterfaceMétier] peut aussi calculer le montant de l’impôt d’un ensemble de contribuables dont les données sont rassemblées dans le fichier texte nommé [$taxPayersFileName]. Elle met les résultats dans un fichier texte nommé [$resultsFileName]. La méthode [executeBatchImpots] doit s’adresser à la couche [dao] qui s’occupe des accès au système de fichiers. Des exceptions peuvent alors remonter de la couche [dao] que la méthode [executeBatchImpots] n’interceptera pas : elle les laissera remonter au script principal. Les erreurs non fatales sont enregistrées dans le fichier texte nommé [$errorsFileName] ;
  • ligne 9 : la méthode [calculerImpot] est une méthode purement [métier]. Elle ne se préoccupe pas d’où viennent les données qu’elle utilise ;
  • ligne 12 : la méthode [executeBatchImpots] va s’adresser à la couche [dao] pour lire et écrire des données dans des fichiers texte. Elle va appeler de façon répétée la méthode métier [calculerImpot] ;

XI-D-2. La classe [Metier]

La classe [Metier] implémente l’interface [InterfaceMetier] de la façon suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
<?php

// espace de noms
namespace Application;

class Metier implements InterfaceMetier {
  // couche Dao
  private $dao;
  // données administration fiscale
  private $taxAdminData;

  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // on mémorise une référence sur la couche [dao]
    $this->dao = $dao;
    // on récupère les données permettant le calcul de l'impôt
    // la méthode [getTaxAdminData] peut lancer une exception ExceptionImpots
    // on la laisse alors remonter au code appelant
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// calcul de l'impôt
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {// résultat
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {// résultat
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=salaireAnnuel-abattement
  // l'abattement a un min et un max
  private function getRevenuImposable(float $salaire): float {// résultat
    return floor($revenuImposable);
  }

// calcule une décôte éventuelle
  private function getDecôte(string $marié, float $salaire, float $impots): float {// résultat
    return ceil($décôte);
  }

// calcule une réduction éventuelle
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {// résultat
    return ceil($réduction);
  }

  // calcul des impôts en mode batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {// enregistrement des résultats
    $this->dao->saveResults($resultsFileName, $results);
  }

}

Commentaires

  • ligne 6 : la classe [Metier] implémente l’interface [InterfaceMetier], ç-à-d les méthodes [calculerImpot] (lignes 30-34) et [executeBatchImpots] (lignes 66-70) ;
  • ligne 8 : une référence sur la couche [dao]. Il en faut obligatoirement une pour que la couche [métier] sache à qui s’adresser lorsqu’elle veut des données externes. Cet attribut sera initialisé via le setter des lignes 14-17 ou via le constructeur des lignes 19-26 ;
  • ligne 10 : l’objet de type [TaxAdminData] qui encapsule les données de l’administration fiscale. Ces données sont nécessaires à la méthode métier [calculerImpot]. Cet attribut est initialisé via le constructeur des lignes 19-26 ;
  • lignes 19-26 : le constructeur initialise les deux attributs de la classe :
    • l’attribut [$dao] est initialisé avec la référence passée en paramètre au constructeur. On notera que le type de ce paramètre est celui de l’interface [InterfaceDao] permettant ainsi à la classe [Metier] d’être initialisée par n’importe quelle classe implémentant cette interface ;
    • l’attribut [$taxAdminData] est initialisé en faisant appel à la méthode [getTaxAdminData] de la couche [dao] ;

On en conclut que lorsque les méthodes [calculerImpots] et [executeBatchImpots] s’exécutent, les deux attributs [$dao] et [$taxAdminData] sont initialisés.

La méthode [calculerImpots] est la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié : oui, non
    // $enfants : nombre d'enfants
    // $salaire : salaire annuel
    // $this->taxAdminData : données de l'administration fiscale
    //
    // on vérifie qu'on a bien les données de l'administration fiscale
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // calcul de l'impôt avec enfants
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // calcul de l'impôt sans les enfants
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // application du plafonnement du quotient familial
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // on prend l'impôt le plus fort
    if ($impot1 > $impot2) {
      $impot = $impot1;
      $taux = $result1["taux"];
      $surcôte = $result1["surcôte"];
    } else {
      $surcôte = $impot2 - $impot1 + $result2["surcôte"];
      $impot = $impot2;
      $taux = $result2["taux"];
    }
    // calcul d'une éventuelle décôte
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // calcul d'une éventuelle réduction d'impôts
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // résultat
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

Commentaires

  • ce code est celui de la méthode [AbstractBaseImpots::calculerImpot] de la version 3, expliquée au paragraphe lienLa classe abstraite [AbstractBaseImpots]. Il en est de même pour les méthodes privées [calculerImpot2, getDecôte, getRéduction, getRevenuImposable] ;

La méthode [Metier::executeBatchImpots] est la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // on laisse remonter les exceptions qui proviennent de la couche [dao]
    // on récupère les données contribuables
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tableau des résultats
    $results = [];
    // on les exploite
    foreach ($taxPayersData as $taxPayerData) {
      // on calcule l'impôt
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // on complète [$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"]);
      // on met le résultat dans le tableau des résultats
      $results [] = $taxPayerData;
    }
    // enregistrement des résultats
    $this->dao->saveResults($resultsFileName, $results);
  }

Commentaires

  • ligne 1 : la méthode doit appeler de façon répétée la méthode [calculerImpot] pour chacun des contribuables trouvés dans le fichier texte nommé [$taxPayersFileName]. Elle doit mettre les résultats dans le fichier texte nommé [$resultsFileName]. Les erreurs non fatales rencontrées sont enregistrées dans dans le fichier texte nommé [$errorsFileName]. La méthode ne lance pas d’exceptions elle-même mais laisse remonter celles que la couche [dao] lance ;
  • ligne 4 : les données des contribuables sont demandées à la couche [dao]. Celle-ci renvoie un tableau d’élements de type [TaxPayerData] qui est une classe d’attributs [marié, nbEnfants, salaire, montant, décôte, réduction, surcôte, taux] (cf paragraphe lienObjets échangés entre couches). S’il se produit une exception ici, comme elle n’est pas interceptée par un catch, elle remontera automatiquement au code appelant. Cela signifie qu’en cas d’exception, la ligne 6 n’est pas exécutée ;
  • ligne 6 : le tableau des résultats de type [TaxPayerData] ;
  • lignes 8-22 : on calcule l’impôt pour chacun des éléments du tableau des contribuables [$taxPayersData]. Pour cela, on fait appel à la méthode interne [calculerImpot] (ligne 10) ;
  • lignes 15-19 : le résultat obtenu est utilisé pour initialiser les attributs de [TaxPayerData] qui ne l’étaient pas encore ;
  • ligne 21 : le résultat obtenu est cumulé dans le tableau des résultats [$results] ;
  • ligne 24 : une fois l’impôt calculé pour tous les contribuables, les résultats sont mémorisés dans un fichier texte. C’est la couche [dao] qui fait ce travail ;

Conclusion

En général la couche [métier] est assez simple à écrire car elle s’adresse à la couche [dao] qui, elle, gère l’accès aux données avec la gestion des erreurs qui va avec.

XI-E. Le script principal

On écrit maintenant le script de la couche [3] de notre architecture :

Image non disponible

Le script principal est le suivant [main.php] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
<?php

// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);

// espace de noms
namespace Application;

// gestion des erreurs par PHP
//ini_set("display_errors", "0");

// inclusion interface et classes
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// définition des constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // création de la couche [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // création de la couche [métier]
  $métier = new Metier($dao);
  // calcul de l'impôts en mode batch
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // on affiche l'erreur
  print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;

Commentaires

  • ligne 24 : le nom du fichier des données contribuables ;
  • ligne 25 : le nom du fichier des résultats ;
  • ligne 26 : le nom du fichier des erreurs ;
  • ligne 27 : le nom du fichier jSON contenant les données de l’administration fiscale;
  • ligne 31 : création de la couche [dao] ;
  • ligne 33 : création de la couche [métier] s’appuyant sur cette couche [dao] ;
  • ligne 35 : exécution de la méthode [executeBatchImpots] de la couche [métier] ;
  • lignes 36-39 : on a vu que la couche [métier] pouvait remonter des exceptions. Elles sont interceptées ici ;

XI-F. Tests visuels

XI-F-1. Test n° 1

Avec le fichier des contribuables [taxpayersdata.txt] suivants :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
oui,2,55555
oui,2,50000
oui,3,50000
non,2,100000
non,3x,100000
oui,3,100000
oui,5,100000x
non,0,100000
oui,2,30000
non,0,200000
oui,3,200000

on obtient le fichier des erreurs [errors.txt] suivant :

 
Sélectionnez
1.
2.
la ligne [5] du fichier [taxpayersdata.txt] est erronée
la ligne [7] du fichier [taxpayersdata.txt] est erronée

et le fichier des résultats [resultats.txt] suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
{"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é":"oui","enfants":3,"salaire":100000,"impôt":9200,"surcôte":2180,"décôte":0,"réduction":0,"taux":0.3}
{"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":200000,"impôt":42842,"surcôte":17283,"décôte":0,"réduction":0,"taux":0.41}

XI-F-2. Test n° 2

Dans le script principal, on met pour le fichier des contribuables un nom de fichier qui n’existe pas :

 
Sélectionnez
const TAXPAYERS_DATA_FILENAME = "taxpayersdata2.txt";

Les résultats obtenus à la console alors sont les suivants :

 
Sélectionnez
1.
2.
3.
4.
Warning: fopen(taxpayersdata2.txt): failed to open stream: No such file or directory in C:\Data\st-2019\dev\php7\poly\scripts-console\impots\version-04\TraitDao.php on line 18
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé
Done.
  • ligne 1 : avertissements (warning) de l’interpréteur PHP ;
  • ligne 2 : le message d’erreur de l’exception lancée par la couche [dao] ;

Il est possible de mettre en sourdine les messages d’erreur de l’interpréteur PHP :

Image non disponible

La ligne 21 du code ci-dessus demande à ce que les erreurs PHP ne soient pas affichées. Pendant la phase de développement il est nécessaire qu’elles soient affichées. En mode production, il faut les cacher.

Les résultats de l’exécution sont alors les suivants :

 
Sélectionnez
1.
2.
Impossible d'ouvrir en lecture les déclarations des contribuables [taxpayersdata2.txt]
Terminé

XI-G. Tests [Codeception]

Les tests visuels sont très insuffisants :

  • on se limite en général à quelques tests ;
  • on est plus ou moins attentif lors de cette vérification visuelle et des détails peuvent nous échapper ;

Dans la réalité du développement professionnel, les tests sont rédigés par des personnes dédiées dont c’est le rôle principal. Elles cherchent alors à faire les tests les plus complets possible(s). Pour cela elles utilisent des frameworks de test.

Nous allons ici utiliser le framework Codeception [https://codeception.com/] car il peut être intégré à Netbeans. C’est un framework avec un large éventail de possibilités. Nous n’allons en utiliser que quelques-unes. L’idée est d’avoir un moyen rapide, après chaque nouvelle version de l’exercice d’application, de vérifier que celle-ci fonctionne. L’existence de tests réussis donne au développeur confiance dans le code qu’il a écrit. C’est un facteur important.

XI-G-1. Installation du framework [Codeception]

Comme beaucoup de bibliothèques PHP, le framework [Codeception] s’installe avec [Composer]. Nous ouvrons donc un terminal Laragon (cf paragraphe lienInstallation de Laragon).

Il nous faut tout d’abord installer le framework de tests PHPUnit [https://phpunit.de/]. En effet Codeception utilise en sous-main le framework PHPUnit:

Image non disponible

Ensuite, nous installons le framework Codeception :

Image non disponible

C’est tout. Maintenant voyons l’intégration de [Codeception] dans Netbeans.

XI-G-2. Intégration de [CodeCeption] dans Netbeans

Image non disponible
  • en [1-2], on accède aux propriétés du projet ;
  • en [3-4], on fait de [Codeception] l’un des frameworks de test du projet ;
Image non disponible
Image non disponible
  • en [5-8], on initialise le framework [Codeception] pour le projet ;
Image non disponible
  • en [9], un dossier [tests] a été créé, ainsi qu’un fichier de configuration [codeception.yml] en [10-11]. Le fichier [11] est le même que le fichier [10]. Codeception a simplement créé un dossier [Important Files] pour donner une signification particulière au fichier [10] ;
  • en [12-13], on revient aux propriétés du projet ;
Image non disponible
  • en [14-16], on désigne le dossier [tests] [16], comme le dossier de tests du projet ;
  • en [16], le dossier [tests] apparaît alors sous le nouveau nom [Test Files]. La présence de ce dossier dans un projet PHP montre que ce projet intègre un framework de tests programmés ;
  • nous créerons nos tests dans le dossier [unit] [17] ;

XI-G-3. Tests de la couche [dao]

Image non disponible
  • nous allons créer tous nos tests dans le dossier [unit] [1] ;
  • les noms des classes de test [Codeception] doivent se terminer par le mot clé [Test], sinon les classes ne seront pas reconnues comme classes de test ;

Nos classes de test [Codeception] auront la forme suivante [https://codeception.com/docs/05-UnitTests] :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
<?php

// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);

// espace de noms
namespace Application;

// chargement de l’environnement de testclass DaoTest extends \Codeception\Test\Unit {
  // attributs du test
  private $attribut1;

  public function __construct() {
    parent::__construct();
    // initialisation de l’environnement de test}

  // tests
  public function testTaxAdminData() {
    // tests
    $this->assertEquals($expected, $actual);
    $this->assertEqualsWithDelta($expected, $actual, $delta);
    $this->assertTrue($actual);
    $this->assertFalse($actual);
    $this->assertNull($actual);
    $this->assertEmpty($actual);
    $this->assertSame($expected, $actual);}

}

Commentaires

  • ligne 7 : les classes de test seront dans le même espace de noms que l’application testée ;
  • lignes 9-10 : ici on trouvera les opérations [require] pour charger les classes et interfaces testées ;
  • ligne 12 : le nom de la classe de test doit obligatoirement se terminer par le mot clé [Test]. Cette classe doit étendre la classe [\Codeception\Test\Unit] ;
  • lignes 16-20 : le constructeur nous permettra d’initialiser l’environnement du test ;
  • ligne 23 : les noms des méthodes de test doivent obligatoirement commencer par le mot clé [test] ;
  • lignes 25-31 : diverses méthodes de test peuvent être utilisées ;

La classe de test [DaoTest] sera la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
<?php

// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);

// espace de noms
namespace Application;

// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// définition des constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    parent::__construct();
    // création de la couche [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // tests
  public function testTaxAdminData() {}
}

Commentaires

Pour construire les tests d’une version de l’exercice d’application, nous utiliserons un environnement identique à celui utilisé par le script principal de la version. Celui de la version 04 est le script [main.php] suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
<?php

// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);

// espace de noms
namespace Application;

// gestion des erreurs par PHP
ini_set("display_errors", "0");

// inclusion interface et classes
require_once __DIR__ . "/TaxAdminData.php";
require_once __DIR__ . "/TaxPayerData.php";
require_once __DIR__ . "/ExceptionImpots.php";
require_once __DIR__ . "/Utilitaires.php";
require_once __DIR__ . "/InterfaceDao.php";
require_once __DIR__ . "/TraitDao.php";
require_once __DIR__ . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once __DIR__ . "/InterfaceMetier.php";
require_once __DIR__ . "/Metier.php";
// test -----------------------------------------------------
// définition des constantes
const TAXPAYERSDATA_FILENAME = "taxpayersdata.txt";
const RESULTS_FILENAME = "resultats.txt";
const ERRORS_FILENAME = "errors.txt";
const TAXADMINDATA_FILENAME = "taxadmindata.json";

try {
  // création de la couche [dao]
  $dao = new DaoImpotsWithTaxAdminDataInJsonFile(TAXADMINDATA_FILENAME);
  // création de la couche [métier]
  $métier = new Metier($dao);
  // calcul de l'impôts en mode batch
  $métier->executeBatchImpots(TAXPAYERSDATA_FILENAME, RESULTS_FILENAME, ERRORS_FILENAME);
} catch (ExceptionImpots $ex) {
  // on affiche l'erreur
  print $ex->getMessage() . "\n";
}
// fin
print "Terminé\n";
exit;

Pour tester la couche [dao], dans la classe de test :

  • nous reprenons l’environnement des lignes 13-27 de [main.php] ;
  • dans le constructeur de la classe de test, nous construisons la couche [dao] comme en ligne 31 ;
  • nous écrivons les méthodes de tests;

Nous procéderons de cette façon pour toutes les classes de test.

Revenons au code complet de la classe de test :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
<?php

// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);

// espace de noms
namespace Application;

// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// définition des constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class DaoTest extends \Codeception\Test\Unit {
  // TaxAdminData
  private $taxAdminData;

  public function __construct() {
    parent::__construct();
    // création de la couche [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    $this->taxAdminData = $dao->getTaxAdminData();
  }

  // tests
  public function testTaxAdminData() {
    // constantes de calcul
    $this->assertEquals(1551, $this->taxAdminData->getPlafondQfDemiPart());
    $this->assertEquals(21037, $this->taxAdminData->getPlafondRevenusCelibatairePourReduction());
    $this->assertEquals(42074, $this->taxAdminData->getPlafondRevenusCouplePourReduction());
    $this->assertEquals(3797, $this->taxAdminData->getValeurReducDemiPart());
    $this->assertEquals(1196, $this->taxAdminData->getPlafondDecoteCelibataire());
    $this->assertEquals(1970, $this->taxAdminData->getPlafondDecoteCouple());
    $this->assertEquals(1595, $this->taxAdminData->getPlafondImpotCelibatairePourDecote());
    $this->assertEquals(2627, $this->taxAdminData->getPlafondImpotCouplePourDecote());
    $this->assertEquals(12502, $this->taxAdminData->getAbattementDixPourcentMax());
    $this->assertEquals(437, $this->taxAdminData->getAbattementDixPourcentMin());
    // tranches de l'impôt
    $this->assertSame([9964.0, 27519.0, 73779.0, 156244.0, 0.0], $this->taxAdminData->getLimites());
    $this->assertSame([0.0, 0.14, 0.30, 0.41, 0.45], $this->taxAdminData->getCoeffR());
    $this->assertSame([0.0, 1394.96, 5798.0, 13913.69, 20163.45], $this->taxAdminData->getCoeffN());
  }

}

Commentaires

  • lignes 10-25 : chargement de l’environnement nécessaire aux tests et définitions de constantes ;
  • lignes 31-36 : construction de la couche [dao], ligne 34, puis initialisation de l’attribut [$taxAdminData] de la ligne 29. Cet attribut contient les données de l’administration fiscale ;
  • lignes 39-55 : l’unique méthode de test. Celui-ci consiste à vérifier que le contenu de l’attribut [$taxAdminData] correspond à ce qui est attendu ;
  • lignes 41-50 : vérifications des constantes du calcul de l’impôt ;
  • lignes 52-55 : vérifications des tranches d’imposition. La méthode [assertSame] vérifie que deux entités PHP, ici des tableaux, sont identiques ;

Pour exécuter cette classe de test, on procède de la façon suivante :

Image non disponible
  • en [1-2], on exécute le test ;
  • [3] : la fenêtre des résultats des tests ;
  • [4] : la classe de test exécutée ;
  • [5] : les résultats. Ici l’unique méthode de test a été réussie ;
  • [6] : lorsque le test échoue ou plus fréquemment lorsqu’aucun test n’a été exécuté, il faut aller voir la fenêtre [6]. Le plus souvent, c’est le chargement de l’environnement du test qui a échoué et aucun test n’a alors pu être exécuté. Les erreurs affichées dans [6] sont celles qu’on aurait avec l’exécution d’un script PHP classique ;

Montrons un exemple de test erroné :

Dans la classe de test, nous introduisons une erreur dans la définition d’une constante :

// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04x");

puis nous exécutons le test. Le résultat obtenu est le suivant :

Image non disponible

Dans la fenêtre [4] :

Image non disponible

XI-G-4. Tests de la couche [métier]

La classe de test [MetierTest] suit les mêmes règles de construction que la classe [DaoTest] mais il y a plus de méthodes de test :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
<?php

// respect strict des types déclarés des paramètres de foctions
declare (strict_types=1);

// espace de noms
namespace Application;

// constantes
define("ROOT", "C:/Data/st-2019/dev/php7/poly/scripts-console/impots/version-04");
define("VENDOR", "C:/myprograms/laragon-lite/www/vendor");
// inclusion interface et classes
require_once ROOT . "/TaxAdminData.php";
require_once ROOT . "/TaxPayerData.php";
require_once ROOT . "/ExceptionImpots.php";
require_once ROOT . "/Utilitaires.php";
require_once ROOT . "/InterfaceDao.php";
require_once ROOT . "/TraitDao.php";
require_once ROOT . "/DaoImpotsWithTaxAdminDataInJsonFile.php";
require_once ROOT . "/InterfaceMetier.php";
require_once ROOT . "/Metier.php";
require_once VENDOR. "/autoload.php";;
// test -----------------------------------------------------
// définition des constantes
const TAXADMINDATA_FILENAME = "taxadmindata.json";

class MetierTest extends \Codeception\Test\Unit {
  // couche métier
  private $métier;

  public function __construct() {
    parent::__construct();
    // création de la couche [dao]
    $dao = new DaoImpotsWithTaxAdminDataInJsonFile(ROOT . "/" . TAXADMINDATA_FILENAME);
    // création de la couche [métier]
    $this->métier = new Metier($dao);
  }

  // tests
  public function test1() {
    $result = $this->métier->calculerImpot("oui", 2, 55555);
    $this->assertEqualsWithDelta(2815, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test2() {
    $result = $this->métier->calculerImpot("oui", 2, 50000);
    $this->assertEqualsWithDelta(1385, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(384, $result["décôte"], 1);
    $this->assertEqualsWithDelta(347, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test3() {
    $result = $this->métier->calculerImpot("oui", 3, 50000);
    $this->assertEqualsWithDelta(0, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(720, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test4() {
    $result = $this->métier->calculerImpot("non", 2, 100000);
    $this->assertEqualsWithDelta(19884, $result["impôt"], 1);
    $this->assertEqualsWithDelta(4480, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test5() {
    $result = $this->métier->calculerImpot("non", 3, 100000);
    $this->assertEqualsWithDelta(16782, $result["impôt"], 1);
    $this->assertEqualsWithDelta(7176, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test6() {
    $result = $this->métier->calculerImpot("oui", 3, 100000);
    $this->assertEqualsWithDelta(9200, $result["impôt"], 1);
    $this->assertEqualsWithDelta(2180, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.3, $result["taux"]);
  }

  public function test7() {
    $result = $this->métier->calculerImpot("oui", 5, 100000);
    $this->assertEqualsWithDelta(4230, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.14, $result["taux"]);
  }

  public function test8() {
    $result = $this->métier->calculerImpot("non", 0, 100000);
    $this->assertEqualsWithDelta(22986, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }

  public function test9() {
    $result = $this->métier->calculerImpot("oui", 2, 30000);
    $this->assertEqualsWithDelta(0, $result["impôt"], 1);
    $this->assertEqualsWithDelta(0, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0, $result["taux"]);
  }

  public function test10() {
    $result = $this->métier->calculerImpot("non", 0, 200000);
    $this->assertEqualsWithDelta(64210, $result["impôt"], 1);
    $this->assertEqualsWithDelta(7498, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.45, $result["taux"]);
  }

  public function test11() {
    $result = $this->métier->calculerImpot("oui", 3, 200000);
    $this->assertEqualsWithDelta(42842, $result["impôt"], 1);
    $this->assertEqualsWithDelta(17283, $result["surcôte"], 1);
    $this->assertEqualsWithDelta(0, $result["décôte"], 1);
    $this->assertEqualsWithDelta(0, $result["réduction"], 1);
    $this->assertEquals(0.41, $result["taux"]);
  }
}

Commentaires

  • lignes 10-25 : chargements des fichiers définissant l’environnement du test. Celui-ci est le même que pour la couche [dao] ;
  • lignes 31-37 : instanciation des couches [dao] et [métier] ;
  • lignes 40-47 : un test de calcul d’impôt ;
  • ligne 41 : un certain calcul d’impôt est fait avec la couche [métier] ;
  • lignes 42-46 : on vérifie que les résultats obtenus sont ceux du simulateur de l’administration fiscale [https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm] ;
  • lignes 23-26 : les tests d’égalité sont faits à 1 euro près. En effet, on a vu que des problèmes d’arrondi faisaient que l’algorithme du document donnait les résultats attendus à 1 euro près ;
  • ligne 27 : le taux d’imposition est lui calculé sans marge d’erreur ;
  • lignes 49-137 : on répète ce type de tests 10 fois avec à chaque fois une configuration du contribuable différente ;

Les tests donnent les résultats suivants :

Image non disponible

XI-G-5. Tests des prochaines versions

Dans la suite, les tests des couches [dao] et [métier] seront identiques à ceux de la version 04. Seul changera l’environnement du test. Nous ne présenterons donc que celui-ci et les résultats des tests.


précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Serge Tahé et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2019 Developpez.com.