14. Clientes HTTP javascript del servicio de cálculo de impuestos
14.1. Introducción
Aquí se propone escribir un [Node.js] cliente para la versión 14 del servicio de cálculo de impuestos. La arquitectura cliente/servidor será la siguiente:

Examinaremos dos versiones del cliente:
- La versión 1 del cliente tendrá la siguiente [main, dao] estructura en capas:

- La versión 2 del cliente tendrá una [main, business logic, DAO] estructura. La [lógica de negocio] capa del servidor se trasladará al cliente:

14.2. Cliente HTTP 1

Como se ha mencionado, el cliente HTTP 1 implementa la siguiente arquitectura cliente/servidor:

Ejecutaremos:
- la [DAO] capa como clase;
- la [main] capa como un script que utiliza esta clase;
14.2.1. La capa [dao]
La [dao] capa será implementada por la siguiente clase [Dao1.js]:
1. 'use strict';
2.
3. // imports
4. import qs from 'qs'
5.
6. class Dao1 {
7.
8. // constructor
9. constructor(axios) {
10. // axios library for making HTTP requests
11. this.axios = axios;
12. // session cookie
13. this.sessionCookieName = "PHPSESSID";
14. this.sessionCookie = '';
15. }
16.
17. // initialize session
18. async initSession() {
19. // HTTP request options [get /main.php?action=init-session&type=json]
20. const options = {
21. method: "GET",
22. // URL parameters
23. params: {
24. action: 'init-session',
25. type: 'json'
26. }
27. };
28. // Execute the HTTP request
29. return await this.getRemoteData(options);
30. }
31.
32. async authenticateUser(user, password) {
33. // HTTP request options [post /main.php?action=authenticate-user]
34. const options = {
35. method: "POST",
36. headers: {
37. 'Content-type': 'application/x-www-form-urlencoded',
38. },
39. // POST body
40. data: qs.stringify({
41. user: user,
42. password: password
43. }),
44. // URL parameters
45. params: {
46. action: 'authenticate-user'
47. }
48. };
49. // execute the HTTP request
50. return await this.getRemoteData(options);
51. }
52.
53. // calculate tax
54. async calculateTax(married, children, salary) {
55. // HTTP request options [post /main.php?action=calculate-tax]
56. const options = {
57. method: "POST",
58. headers: {
59. 'Content-type': 'application/x-www-form-urlencoded',
60. },
61. // POST body [married, children, salary]
62. data: qs.stringify({
63. married: married,
64. children: children,
65. salary: salary
66. }),
67. // URL parameters
68. params: {
69. action: 'calculate-tax'
70. }
71. };
72. // execute the HTTP request
73. const data = await this.getRemoteData(options);
74. // result
75. return data;
76. }
77.
78. // list of simulations
79. async listSimulations() {
80. // HTTP request options [get /main.php?action=list-simulations]
81. const options = {
82. method: "GET",
83. // URL parameters
84. params: {
85. action: 'list-simulations'
86. },
87. };
88. // execute the HTTP request
89. const data = await this.getRemoteData(options);
90. // result
91. return data;
92. }
93.
94. // list of simulations
95. async deleteSimulation(index) {
96. // HTTP request options [get /main.php?action=delete-simulation&number=index]
97. const options = {
98. method: "GET",
99. // URL parameters
100. params: {
101. action: 'delete-simulation',
102. number: index
103. },
104. };
105. // execute the HTTP request
106. const data = await this.getRemoteData(options);
107. // result
108. return data;
109. }
110.
111. async getRemoteData(options) {
112. // for the session cookie
113. if (!options.headers) {
114. options.headers = {};
115. }
116. options.headers.Cookie = this.sessionCookie;
117. // execute the HTTP request
118. let response;
119. try {
120. // asynchronous request
121. response = await this.axios.request('main.php', options);
122. } catch (error) {
123. // The [error] parameter is an exception instance—it can take various forms
124. if (error.response) {
125. // the server's response is in [error.response]
126. response = error.response;
127. } else {
128. // we throw the error again
129. throw error;
130. }
131. }
132. // response is the entire HTTP response from the server (HTTP headers + the response itself)
133. // retrieve the session cookie if it exists
134. const setCookie = response.headers['set-cookie'];
135. if (setCookie) {
136. // setCookie is an array
137. // we search for the session cookie in this array
138. let found = false;
139. let i = 0;
140. while (!found && i < setCookie.length) {
141. // search for the session cookie
142. const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
143. if (results) {
144. // store the session cookie
145. // eslint-disable-next-line require-atomic-updates
146. this.sessionCookie = results[1];
147. // found
148. found = true;
149. } else {
150. // next element
151. i++;
152. }
153. }
154. }
155. // The server's response is in [response.data]
156. return response.data;
157. }
158. }
159.
160. // export the class
161. export default Dao1;
- Aquí estamos usando lo aprendido en la sección enlazado, donde introdujimos la [axios] biblioteca, que nos permite hacer peticiones HTTP tanto en [node.js] como en un navegador. Veremos específicamente el script en la sección enlazado;
- líneas 9-15: el constructor de la clase. Esta clase tendrá tres propiedades:
- [axios]: el [axios] objeto utilizado para realizar peticiones HTTP. Esto es pasado por el código de llamada;
- [sessionCookieName]: dependiendo del servidor, la cookie de sesión tiene diferentes nombres. Aquí es [PHPSESSID];
- [sessionCookie]: la cookie de sesión enviada por el servidor y almacenada por el cliente;
- líneas 53-76: la función asíncrona [calcularImpuesto] realiza la petición [post /main.php?action=calcular-impuesto] colocando los parámetros [casado, hijos, salario]. Devuelve la cadena JSON enviada por el servidor como un objeto JavaScript;
- líneas 79-92: la función asíncrona [listSimulaciones] realiza la petición [get /main.php?action=list-simulaciones]. Devuelve la cadena JSON enviada por el servidor como un objeto JavaScript;
- líneas 95-109: La función asíncrona [deleteSimulation] realiza la petición [get /main.php?action=delete-simulation&number=index]. Devuelve la cadena JSON enviada por el servidor como un objeto JavaScript;
- línea 121: se utiliza la notación [this.axios] porque aquí, el [axios] objeto pasado al constructor se ha almacenado en la [this.axios] propiedad;
- línea 161: la [Dao1] clase se exporta para que pueda ser utilizada;
14.2.2. El script [main1.js]
El [main1.js] script realiza una serie de llamadas al servidor utilizando la [Dao1] clase:
- inicialización de una sesión JSON;
- autenticación con [admin, admin];
- solicita tres cálculos de impuestos;
- solicita la lista de simulaciones;
- elimina uno de ellos;
El código es el siguiente:
1. // import axios
2. import axios from 'axios';
3. // import the Dao1 class
4. import Dao from './Dao1';
5.
6. // asynchronous function [main]
7. async function main() {
8. // Axios configuration
9. axios.defaults.timeout = 2000;
10. axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
11. // instantiate the [dao] layer
12. const dao = new Dao(axios);
13. // using the [dao] layer
14. try {
15. // initialize session
16. log("-----------init-session");
17. let response = await dao.initSession();
18. log(response);
19. // authentication
20. log("-----------authenticate-user");
21. response = await dao.authenticateUser("admin", "admin");
22. log(response);
23. // tax calculations
24. log("-----------calculate-tax x 3");
25. response = await Promise.all([
26. dao.calculateTax("yes", 2, 45000),
27. dao.calculateTax("no", 2, 45000),
28. dao.calculateTax("no", 1, 30000)
29. ]);
30. log(response);
31. // list of simulations
32. log("-----------list-of-simulations");
33. response = await dao.listSimulations();
34. log(response);
35. // Delete a simulation
36. log("-----------deleting simulation #1");
37. response = await dao.deleteSimulation(1);
38. log(response);
39. } catch (error) {
40. // log the error
41. console.log("error=", error.message);
42. }
43. }
44.
45. // log JSON
46. function log(object) {
47. console.log(JSON.stringify(object, null, 2));
48. }
49.
50. // execution
51. main();
Comentarios
- línea 2: importar la [axios] biblioteca;
- línea 4: importar la [Dao] clase;
- línea 7: la [main] función que se comunica con el servidor es asíncrona;
- líneas 9-10: configuración por defecto para que las peticiones HTTP se envíen al servidor:
- línea 9: [tiempo de espera] de 2 segundos;
- línea 10: todas las URL llevan como prefijo la URL base de la versión 14 del servidor de cálculo de impuestos;
- línea 12: la [Dao] capa está construida. Ahora se puede utilizar;
- líneas 46-48: la función [log] se utiliza para mostrar la cadena JSON de un objeto JavaScript de forma formateada: verticalmente con dos espacios de sangría (3er parámetro);
- líneas 15-18: inicialización de la sesión JSON;
- líneas 19-22: autentificación;
- líneas 23-30: se solicitan tres cálculos de impuestos en paralelo. Gracias a [await Promise.all], la ejecución se bloquea hasta obtener los tres resultados;
- líneas 31-34: lista de simulaciones;
- líneas 35-38: supresión de una simulación;
- líneas 39-42: gestión de posibles excepciones;
Los resultados de la ejecución son los siguientes:
1. [Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 1\main1.js"
2. "-----------init-session"
3. {
4. "action": "init-session",
5. "status": 700,
6. "response": "session started with type [json]"
7. }
8. "-----------authenticate-user"
9. {
10. "action": "authenticate-user",
11. "status": 200,
12. "response": "Authentication successful [admin, admin]"
13. }
14. "-----------calculate-tax x 3"
15. [
16. {
17. "action": "calculate-tax",
18. "status": 300,
19. "response": {
20. "married": "yes",
21. "children": "2",
22. "salary": "45000",
23. "tax": 502,
24. "surcharge": 0,
25. "discount": 857,
26. "reduction": 126,
27. "rate": 0.14
28. }
29. },
30. {
31. "action": "calculate-tax",
32. "status": 300,
33. "response": {
34. "married": "no",
35. "children": "2",
36. "salary": "45000",
37. "tax": 3250,
38. "surcharge": 370,
39. "discount": 0,
40. "reduction": 0,
41. "rate": 0.3
42. }
43. },
44. {
45. "action": "calculate-tax",
46. "status": 300,
47. "response": {
48. "married": "no",
49. "children": "1",
50. "salary": "30000",
51. "tax": 1687,
52. "surcharge": 0,
53. "discount": 0,
54. "reduction": 0,
55. "rate": 0.14
56. }
57. }
58. ]
59. "-----------list-of-simulations"
60. {
61. "action": "list-simulations",
62. "status": 500,
63. "response": [
64. {
65. "married": "yes",
66. "children": "2",
67. "salary": "45000",
68. "tax": 502,
69. "surcharge": 0,
70. "discount": 857,
71. "reduction": 126,
72. "rate": 0.14,
73. "arrayOfAttributes": null
74. },
75. {
76. "married": "no",
77. "children": "2",
78. "salary": "45000",
79. "tax": 3250,
80. "surcharge": 370,
81. "discount": 0,
82. "reduction": 0,
83. "rate": 0.3,
84. "arrayOfAttributes": null
85. },
86. {
87. "married": "no",
88. "children": "1",
89. "salary": "30000",
90. "tax": 1687,
91. "surcharge": 0,
92. "discount": 0,
93. "reduction": 0,
94. "rate": 0.14,
95. "arrayOfAttributes": null
96. }
97. ]
98. }
99. "-----------simulation deletion #1"
100. {
101. "action": "delete-simulation",
102. "status": 600,
103. "response": [
104. {
105. "married": "yes",
106. "children": "2",
107. "salary": "45000",
108. "tax": 502,
109. "surcharge": 0,
110. "discount": 857,
111. "reduction": 126,
112. "rate": 0.14,
113. "arrayOfAttributes": null
114. },
115. {
116. "married": "no",
117. "children": "1",
118. "salary": "30000",
119. "tax": 1687,
120. "surcharge": 0,
121. "discount": 0,
122. "reduction": 0,
123. "rate": 0.14,
124. "arrayOfAttributes": null
125. }
126. ]
127. }
128.
129. [Done] exited with code=0 in 0.516 seconds
14.3. Cliente HTTP 2

La arquitectura del cliente HTTP2 es la siguiente:

Hemos movido la [business] capa del servidor al cliente JavaScript. A diferencia de lo que hicimos en el curso de PHP7, la [main] capa no necesitará pasar por la [business] capa para llegar a la [DAO] capa. Utilizaremos estas dos capas como componentes especializados:
- la [main] capa pasa por la [DAO] capa siempre que necesita datos que están en el servidor;
- la [principal] capa pide a la [empresa] capa que realice los cálculos fiscales;
- la [empresa] capa es independiente de la [DAO] capa y nunca la invoca;
14.3.1. La clase JavaScript [Business]
La esencia de la [Business] clase en PHP se describió en el artículo enlazado. Es un trozo de código bastante complejo que recordamos aquí, no para explicarlo, sino para poder traducirlo a JavaScript:
1. <?php
2.
3. // namespace
4. namespace Application;
5.
6. class Business implements BusinessInterface {
7. // DAO layer
8. private $dao;
9. // tax administration data
10. private $taxAdminData;
11.
12. //---------------------------------------------
13. // [dao] layer setter
14. public function setDao(InterfaceDao $dao) {
15. $this->dao = $dao;
16. return $this;
17. }
18.
19. public function __construct(DaoInterface $dao) {
20. // store a reference to the [dao] layer
21. $this->dao = $dao;
22. // retrieve the data needed to calculate the tax
23. // the [getTaxAdminData] method may throw an ExceptionImpots exception
24. // we let it propagate to the calling code
25. $this->taxAdminData = $this->dao->getTaxAdminData();
26. }
27.
28. // calculate the tax
29. // --------------------------------------------------------------------------
30. public function calculateTax(string $married, int $children, int $salary): array {
31. // $marié: yes, no
32. // $children: number of children
33. // $salary: annual salary
34. // $this->taxAdminData: tax administration data
35. //
36. // Check that we have the tax administration data
37. if ($this->taxAdminData === NULL) {
38. $this->taxAdminData = $this->getTaxAdminData();
39. }
40. // Calculate tax with children
41. $result1 = $this->calculateTax2($married, $children, $salary);
42. $tax1 = $result1["tax"];
43. // Calculate tax without children
44. if ($children != 0) {
45. $result2 = $this->calculateTax2($married, 0, $salary);
46. $tax2 = $result2["tax"];
47. // apply the family quotient cap
48. $halfPartCap = $this->taxAdminData->getQfHalfPartCap();
49. if ($children < 3) {
50. // $PLAFOND_QF_DEMI_PART euros for the first 2 children
51. $tax2 = $tax2 - $children * $half-share-limit;
52. } else {
53. // $PLAFOND_QF_DEMI_PART euros for the first 2 children, double that for subsequent children
54. $tax2 = $tax2 - 2 * $half-share_limit - ($children - 2) * 2 * $half-share_limit;
55. }
56. } else {
57. $tax2 = $tax1;
58. $result2 = $result1;
59. }
60. // take the higher tax
61. if ($tax1 > $tax2) {
62. $tax = $tax1;
63. $rate = $result1["rate"];
64. $surcharge = $result1["surcharge"];
65. } else {
66. $surcharge = $tax2 - $tax1 + $result2["surcharge"];
67. $tax = $tax2;
68. $rate = $result2["rate"];
69. }
70. // calculate any tax credit
71. $discount = $this->getDiscount($spouse, $salary, $tax);
72. $tax -= $discount;
73. // calculate any tax reduction
74. $reduction = $this->getReduction($spouse, $salary, $children, $tax);
75. $tax -= $reduction;
76. // result
77. return ["tax" => floor($tax), "surcharge" => $surcharge, "discount" => $discount, "reduction" => $reduction, "rate" => $rate];
78. }
79.
80. // --------------------------------------------------------------------------
81. private function calculateTax2(string $married, int $children, float $salary): array {
82. // $marié: yes, no
83. // $children: number of children
84. // $salary: annual salary
85. // $this->taxAdminData: tax administration data
86. //
87. // number of shares
88. $married = strtolower($married);
89. if ($married === "yes") {
90. $nbParts = $children / 2 + 2;
91. } else {
92. $numberOfShares = $children / 2 + 1;
93. }
94. // 1 share per child starting from the 3rd
95. if ($children >= 3) {
96. // half a portion more for each child starting with the third
97. $nbParts += 0.5 * ($children - 2);
98. }
99. // taxable income
100. $taxableIncome = $this->getTaxableIncome($salary);
101. // surcharge
102. $surcharge = floor($taxableIncome - 0.9 * $salary);
103. // for rounding issues
104. if ($surcharge < 0) {
105. $surplus = 0;
106. }
107. // family quotient
108. $quotient = $taxableIncome / $numberOfShares;
109. // tax calculation
110. $limits = $this->taxAdminData->getLimits();
111. $coeffR = $this->taxAdminData->getCoeffR();
112. $coeffN = $this->taxAdminData->getCoeffN();
113. // is placed at the end of the limits array to stop the following loop
114. $limits[count($limits) - 1] = $quotient;
115. // look up the tax rate
116. $i = 0;
117. while ($quotient > $limits[$i]) {
118. $i++;
119. }
120. // Since we placed $quotient at the end of the $limites array, the previous loop
121. // cannot go beyond the bounds of the $limits array
122. // now we can calculate the tax
123. $tax = floor($taxableIncome * $coeffR[$i] - $numberOfShares * $coeffN[$i]);
124. // result
125. return ["tax" => $tax, "surcharge" => $surcharge, "rate" => $coeffR[$i]];
126. }
127.
128. // taxableIncome = annualSalary - deduction
129. // The deduction has a minimum and a maximum
130. private function getTaxableIncome(float $salary): float {
131. // 10% deduction from salary
132. $deduction = 0.1 * $salary;
133. // this deduction cannot exceed $this->taxAdminData->getMaxTenPercentDeduction()
134. if ($deduction > $this->taxAdminData->getMaxTenPercentDeduction()) {
135. $deduction = $this->taxAdminData->getMaxTenPercentDeduction();
136. }
137. // the deduction cannot be less than $this->taxAdminData->getMinTenPercentDeduction()
138. if ($deduction < $this->taxAdminData->getMinTenPercentDeduction()) {
139. $deduction = $this->taxAdminData->getMinTenPercentDeduction();
140. }
141. // taxable income
142. $taxableIncome = $salary - $deduction;
143. // result
144. return floor($taxableIncome);
145. }
146.
147. // calculates a possible tax credit
148. private function getDiscount(string $married, float $salary, float $taxes): float {
149. // initially, a zero discount
150. $discount = 0;
151. // maximum tax amount to qualify for the deduction
152. $taxThresholdForDeduction = $married === "yes" ?
153. $this->taxAdminData->getPlafondImpotCouplePourDecote() :
154. $this->taxAdminData->getSingleTaxLimitForDiscount();
155. if ($taxes < $taxLimitForDiscount) {
156. // maximum deduction amount
157. $discountLimit = $married === "yes" ?
158. $this->taxAdminData->getCoupleDeductionLimit() :
159. $this->taxAdminData->getSingleTaxDeductionLimit();
160. // theoretical deduction
161. $discount = $discountLimit - 0.75 * $taxes;
162. // the deduction cannot exceed the tax amount
163. if ($discount > $taxes) {
164. $discount = $taxes;
165. }
166. // no discount <0
167. if ($discount < 0) {
168. $discount = 0;
169. }
170. }
171. // result
172. return ceil($discount);
173. }
174.
175. // calculates a possible discount
176. private function getDiscount(string $married, float $salary, int $children, float $taxes): float {
177. // the income threshold to qualify for the 20% reduction
178. $IncomeThresholdForReduction = $married === "yes" ?
179. $this->taxAdminData->getIncomeLimitForCoupleDeduction() :
180. $this->taxAdminData->getIncomeLimitForSingleTaxpayersForReduction();
181. $IncomeLimitForReduction += $children * $this->taxAdminData->getHalfShareReductionValue();
182. if ($children > 2) {
183. $IncomeLimitForReduction += ($children - 2) * $this->taxAdminData->getHalfShareReductionValue();
184. }
185. // taxable income
186. $taxableIncome = $this->getTaxableIncome($salary);
187. // deduction
188. $reduction = 0;
189. if ($taxableIncome < $IncomeThresholdForDeduction) {
190. // 20% reduction
191. $reduction = 0.2 * $taxes;
192. }
193. // result
194. return ceil($reduction);
195. }
196.
197. // Calculate taxes in batch mode
198. public function executeBatchTaxes(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
199. // let exceptions from the [DAO] layer propagate
200. // retrieve taxpayer data
201. $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
202. // results array
203. $results = [];
204. // process them
205. foreach ($taxPayersData as $taxPayerData) {
206. // calculate the tax
207. $result = $this->calculateTax(
208. $taxPayerData->isMarried(),
209. $taxPayerData->getChildren(),
210. $taxPayerData->getSalary());
211. // update [$taxPayerData]
212. $taxPayerData->setAmount($result["tax"]);
213. $taxPayerData->setDiscount($result["discount"]);
214. $taxPayerData->setSurcharge($result["surcharge"]);
215. $taxPayerData->setRate($result["rate"]);
216. $taxPayerData->setReduction($result["reduction"]);
217. // add the result to the results array
218. $results[] = $taxPayerData;
219. }
220. // Save results
221. $this->dao->saveResults($resultsFileName, $results);
222. }
223.
224. }
- líneas 19-26: el constructor de la clase PHP. Como dijimos que estábamos construyendo una [business] capa independiente de la [DAO] capa, haremos dos cambios en este constructor en JavaScript:
- no recibirá una instancia de la [DAO] capa (ya no la necesita);
- no solicitará datos fiscales a la [taxAdminData] administración de la [dao] capa: el código llamante pasará estos datos al constructor;
- Líneas 197-122: No implementaremos el [executeBatchImpots] método, cuyo fin último era guardar los resultados de la simulación en un fichero de texto. Queremos un código que funcione tanto en [node.js] como en un navegador. Sin embargo, guardar los datos en el sistema de archivos de la máquina que ejecuta el navegador cliente no es posible;
Dadas estas restricciones, el código de la clase JavaScript [Métier] es el siguiente:
1. 'use strict';
2.
3. // Business class
4. class Business {
5.
6. // constructor
7. constructor(taxAdmindata) {
8. // this.taxAdminData: tax administration data
9. this.taxAdminData = taxAdmindata;
10. }
11.
12. // tax calculation
13. // --------------------------------------------------------------------------
14. calculateTax(married, children, salary) {
15. // married: yes, no
16. // children: number of children
17. // salary: annual salary
18. // this.taxAdminData: data from the tax authority
19. //
20. // tax calculation with children
21. const result1 = this.calculateTax2(married, children, salary);
22. const tax1 = result1["tax"];
23. // Calculate tax without children
24. let result2, tax2, half-share-threshold;
25. if (children !== 0) {
26. result2 = this.calculateTax2(married, 0, salary);
27. tax2 = result2["tax"];
28. // apply the family quotient cap
29. half-share-ceiling = this.taxAdminData.half-share-ceiling;
30. if (children < 3) {
31. // FAMILY QUOTIENT HALF-PART CAP in euros for the first 2 children
32. tax2 = tax2 - children * half-share-limit;
33. } else {
34. // PLAFOND_QF_DEMI_PART euros for the first 2 children, double that for subsequent children
35. tax2 = tax2 - 2 * half-share_limit - (children - 2) * 2 * half-share_limit;
36. }
37. } else {
38. // no tax recalculation
39. tax2 = tax1;
40. result2 = result1;
41. }
42. // take the higher tax from [tax1, tax2]
43. let tax, rate, surcharge;
44. if (tax1 > tax2) {
45. tax = tax1;
46. rate = result1["rate"];
47. surcharge = result1["surcharge"];
48. } else {
49. surcharge = tax2 - tax1 + result2["surcharge"];
50. tax = tax2;
51. rate = result2["rate"];
52. }
53. // calculate a possible discount
54. const discount = this.getDiscount(married, tax);
55. tax -= discount;
56. // calculate any tax reduction
57. const taxCredit = this.getTaxCredit(spouse, income, children, tax);
58. tax -= reduction;
59. // result
60. return {
61. "tax": Math.floor(tax), "surcharge": surcharge, "discount": discount, "reduction": reduction,
62. "rate": rate
63. };
64. }
65.
66. // --------------------------------------------------------------------------
67. calculateTax2(married, children, salary) {
68. // married: yes, no
69. // children: number of children
70. // salary: annual salary
71. // this->taxAdminData: tax administration data
72. //
73. // number of shares
74. married = married.toLowerCase();
75. let nbParts;
76. if (married === "yes") {
77. nbParts = children / 2 + 2;
78. } else {
79. nbParts = children / 2 + 1;
80. }
81. // 1 share per child starting from the 3rd
82. if (children >= 3) {
83. // half a portion more for each child starting from the third
84. nbParts += 0.5 * (children - 2);
85. }
86. // taxable income
87. const taxableIncome = this.getTaxableIncome(salary);
88. // surcharge
89. let surcharge = Math.floor(taxableIncome - 0.9 * salary);
90. // for rounding issues
91. if (overlay < 0) {
92. surplus = 0;
93. }
94. // family quotient
95. const quotient = taxableIncome / numberOfShares;
96. // tax calculation
97. const limits = this.taxAdminData.limits;
98. const coeffR = this.taxAdminData.coeffR;
99. const coeffN = this.taxAdminData.coeffN;
100. // is placed at the end of the limits array to stop the following loop
101. limits[limits.length - 1] = quotient;
102. // search for the tax rate
103. let i = 0;
104. while (quotient > limits[i]) {
105. i++;
106. }
107. // Since quotient is stored at the end of the limits array, the previous loop
108. // cannot go beyond the limits array
109. // now we can calculate the tax
110. const tax = Math.floor(taxableIncome * coeffR[i] - numShares * coeffN[i]);
111. // result
112. return { "tax": tax, "surcharge": surcharge, "rate": coeffR[i] };
113. }
114.
115. // taxableIncome = annualSalary - deduction
116. // the deduction has a minimum and a maximum
117. getTaxableIncome(salary) {
118. // deduction of 10% of the salary
119. let deduction = 0.1 * salary;
120. // this deduction cannot exceed taxAdminData.getMaxTenPercentDeduction()
121. if (allowance > this.taxAdminData.MaxTenPercentAllowance) {
122. deduction = this.taxAdminData.maxTenPercentDeduction;
123. }
124. // the deduction cannot be less than taxAdminData.getMinTenPercentDeduction()
125. if (deduction < this.taxAdminData.minTenPercentDeduction) {
126. deduction = this.taxAdminData.minTenPercentDeduction;
127. }
128. // taxable income
129. const taxableIncome = salary - deduction;
130. // result
131. return Math.floor(taxableIncome);
132. }
133.
134. // calculates a possible tax deduction
135. getTaxDeduction(married, taxes) {
136. // Initially, a discount of zero
137. let discount = 0;
138. // maximum tax amount to qualify for the deduction
139. let taxCeilingForDiscount = married === "yes" ?
140. this.taxAdminData.taxCeilingForMarriedCoupleForDiscount :
141. this.taxAdminData.taxCeilingForSingleForDiscount;
142. let discountCeiling;
143. if (taxes < taxLimitForDeduction) {
144. // maximum discount amount
145. discountLimit = married === "yes" ?
146. this.taxAdminData.coupleDiscountLimit :
147. this.taxAdminData.singleDiscountLimit;
148. // theoretical deduction
149. discount = discountLimit - 0.75 * taxes;
150. // the deduction cannot exceed the tax amount
151. if (discount > taxes) {
152. discount = taxes;
153. }
154. // no discount < 0
155. if (discount < 0) {
156. discount = 0;
157. }
158. }
159. // result
160. return Math.ceil(discount);
161. }
162.
163. // calculates a possible tax deduction
164. getReduction(married, salary, children, taxes) {
165. // the income threshold to qualify for the 20% reduction
166. let incomeThresholdForReduction = married === "yes" ?
167. this.taxAdminData.coupleIncomeLimitForReduction :
168. this.taxAdminData.IncomeLimitForSingleTaxpayers;
169. incomeThresholdForDeduction += children * this.taxAdminData.halfShareDeductionValue;
170. if (children > 2) {
171. IncomeLimitForReduction += (children - 2) * this.taxAdminData.halfShareReductionValue;
172. }
173. // taxable income
174. const taxableIncome = this.getTaxableIncome(salary);
175. // deduction
176. let reduction = 0;
177. if (taxableIncome < incomeThresholdForDeduction) {
178. // 20% reduction
179. reduction = 0.2 * taxes;
180. }
181. // result
182. return Math.ceil(reduction);
183. }
184. }
185.
186. // export the class
187. export default Business;
- El código JavaScript sigue de cerca el código PHP;
- Se exporta la [Business] clase, línea 187;
14.3.2. La clase JavaScript [Dao2]

La [Dao2] clase implementa la [dao] capa del cliente JavaScript anterior de la siguiente manera:
1. 'use strict';
2.
3. // imports
4. import qs from 'qs'
5.
6. class Dao2 {
7.
8. // constructor
9. constructor(axios) {
10. this.axios = axios;
11. // session cookie
12. this.sessionCookieName = "PHPSESSID";
13. this.sessionCookie = '';
14. }
15.
16. // initialize session
17. async initSession() {
18. // HTTP request options [get /main.php?action=init-session&type=json]
19. const options = {
20. method: "GET",
21. // URL parameters
22. params: {
23. action: 'init-session',
24. type: 'json'
25. }
26. };
27. // Execute the HTTP request
28. return await this.getRemoteData(options);
29. }
30.
31. async authenticateUser(user, password) {
32. // HTTP request options [post /main.php?action=authenticate-user]
33. const options = {
34. method: "POST",
35. headers: {
36. 'Content-type': 'application/x-www-form-urlencoded',
37. },
38. // POST body
39. data: qs.stringify({
40. user: user,
41. password: password
42. }),
43. // URL parameters
44. params: {
45. action: 'authenticate-user'
46. }
47. };
48. // Execute the HTTP request
49. return await this.getRemoteData(options);
50. }
51.
52. async getAdminData() {
53. // HTTP request options [get /main.php?action=get-admindata]
54. const options = {
55. method: "GET",
56. // URL parameters
57. params: {
58. action: 'get-admindata'
59. }
60. };
61. // execute the HTTP request
62. const data = await this.getRemoteData(options);
63. // result
64. return data;
65. }
66.
67. async getRemoteData(options) {
68. // for the session cookie
69. if (!options.headers) {
70. options.headers = {};
71. }
72. options.headers.Cookie = this.sessionCookie;
73. // execute the HTTP request
74. let response;
75. try {
76. // asynchronous request
77. response = await this.axios.request('main.php', options);
78. } catch (error) {
79. // The [error] parameter is an exception instance—it can take various forms
80. if (error.response) {
81. // the server's response is in [error.response]
82. response = error.response;
83. } else {
84. // we re-throw the error
85. throw error;
86. }
87. }
88. // response is the entire HTTP response from the server (HTTP headers + the response itself)
89. // retrieve the session cookie if it exists
90. const setCookie = response.headers['set-cookie'];
91. if (setCookie) {
92. // setCookie is an array
93. // we search for the session cookie in this array
94. let found = false;
95. let i = 0;
96. while (!found && i < setCookie.length) {
97. // search for the session cookie
98. const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
99. if (results) {
100. // store the session cookie
101. // eslint-disable-next-line require-atomic-updates
102. this.sessionCookie = results[1];
103. // found
104. found = true;
105. } else {
106. // next element
107. i++;
108. }
109. }
110. }
111. // The server's response is in [response.data]
112. return response.data;
113. }
114. }
115.
116. // export the class
117. export default Dao2;
Comentarios
- La [Dao2] clase implementa sólo tres de las posibles peticiones al servidor de cálculo de impuestos:
- [init-session] (líneas 17-29): para inicializar la sesión JSON;
- [autenticar-usuario] (líneas 31-50): autenticar;
- [get-admindata] (líneas 52-65): para recuperar los datos de la administración tributaria que permitirán el cálculo de impuestos en el lado del cliente;
- líneas 52-65: introducimos una nueva acción [get-admindata] en el servidor. Esta acción no se había implementado hasta ahora. Lo hacemos ahora.
14.3.3. Modificación del servidor de cálculo de impuestos
El servidor de cálculo de impuestos debe implementar una nueva acción. Lo haremos en la versión 14 del servidor. La acción a implementar tiene las siguientes características:
- se solicita mediante una operación [get /main.php?action=get-admindata];
- devuelve la cadena JSON de un objeto que encapsula los datos de la administración tributaria;
Revisaremos cómo añadir una acción a nuestro servidor.
La modificación se realizará en NetBeans:

En [2], modificamos el [config.json] archivo para añadir la nueva acción:
1. {
2. "databaseFilename": "Config/database.json",
3. "corsAllowed": true,
4. "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
5. "relativeDependencies": [
6.
7. "/Entities/BaseEntity.php",
8. "/Entities/Simulation.php",
9. "/Entities/Database.php",
10. "/Entities/TaxAdminData.php",
11. "/Entities/TaxExceptions.php",
12.
13. "/Utilities/Logger.php",
14. "/Utilities/SendAdminMail.php",
15.
16. "/Model/InterfaceServerDao.php",
17. "/Model/ServerDao.php",
18. "/Model/ServerDaoWithSession.php",
19. "/Model/InterfaceServerMetier.php",
20. "/Model/ServerBusiness.php",
21.
22. "/Responses/InterfaceResponse.php",
23. "/Responses/ParentResponse.php",
24. "/Responses/JsonResponse.php",
25. "/Responses/XmlResponse.php",
26. "/Responses/HtmlResponse.php",
27.
28. "/Controllers/InterfaceController.php",
29. "/Controllers/InitSessionController.php",
30. "/Controllers/ListSimulationsController.php",
31. "/Controllers/AuthentifierUtilisateurController.php",
32. "/Controllers/CalculateTaxController.php",
33. "/Controllers/DeleteSimulationController.php",
34. "/Controllers/EndSessionController.php",
35. "/Controllers/DisplayTaxCalculationController.php",
36. "/Controllers/AdminDataController.php"
37. ],
38. "absoluteDependencies": [
39. "C:/myprograms/laragon-lite/www/vendor/autoload.php",
40. "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
41. ],
42. "users": [
43. {
44. "login": "admin",
45. "passwd": "admin"
46. }
47. ],
48. "adminMail": {
49. "smtp-server": "localhost",
50. "smtp-port": "25",
51. "from": "guest@localhost",
52. "to": "guest@localhost",
53. "subject": "Tax calculation server crash",
54. "tls": "FALSE",
55. "attachments": []
56. },
57. "logsFilename": "Logs/logs.txt",
58. "actions":
59. {
60. "init-session": "\\InitSessionController",
61. "authenticate-user": "\\AuthentifierUtilisateurController",
62. "calculate-tax": "\\CalculateTaxController",
63. "list-simulations": "\\ListSimulationsController",
64. "delete-simulation": "\\DeleteSimulationController",
65. "end-session": "\\EndSessionController",
66. "display-tax-calculation": "\\DisplayTaxCalculationController",
67. "get-admindata": "\\AdminDataController"
68. },
69. "types": {
70. "json": "\\JsonResponse",
71. "html": "\\HtmlResponse",
72. "xml": "\\XmlResponse"
73. },
74. "views": {
75. "authentication-view.php": [700, 221, 400],
76. "tax-calculation-view.php": [200, 300, 341, 350, 800],
77. "view-simulation-list.php": [500, 600]
78. },
79. "error-views": "error-views.php"
80. }
La modificación consiste en:
- línea 67: añade la [get-admindata] acción y asóciala a un controlador;
- línea 36: declarar este controlador en la lista de clases a cargar por la aplicación PHP;
El siguiente paso es implementar el [AdminDataController] controlador [3]:
1. <?php
2.
3. namespace Application;
4.
5. // Symfony dependencies
6. use \Symfony\Component\HttpFoundation\Response;
7. use \Symfony\Component\HttpFoundation\Request;
8. use \Symfony\Component\HttpFoundation\Session\Session;
9. // alias for the [dao] layer
10. use \Application\ServerDaoWithSession as ServerDaoWithRedis;
11.
12. class AdminDataController implements InterfaceController {
13.
14. // $config is the application configuration
15. // Processing a Request
16. // accesses the Session and can modify it
17. // $infos is additional information specific to each controller
18. // returns an array [$statusCode, $status, $content, $headers]
19. public function execute(
20. array $config,
21. Request $request,
22. Session $session,
23. array $infos = NULL): array {
24.
25. // There must be a single GET parameter
26. $method = strtolower($request->getMethod());
27. $error = $method !== "get" || $request->query->count() != 1;
28. if ($error) {
29. // log the error
30. $message = "You must use the [get] method with the single [action] parameter in the URL";
31. $status = 1001;
32. // return result to the main controller
33. return [Response::HTTP_BAD_REQUEST, $status, ["response" => $message], []];
34. }
35.
36. // we can work
37. // Redis
38. \Predis\Autoloader::register();
39. try {
40. // [predis] client
41. $redis = new \Predis\Client();
42. // Connect to the server to check if it's available
43. $redis->connect();
44. } catch (\Predis\Connection\ConnectionException $ex) {
45. // Something went wrong
46. // return result with error to the main controller
47. $status = 1050;
48. return [Response::HTTP_INTERNAL_SERVER_ERROR, $status,
49. ["response" => "[redis], " . utf8_encode($ex->getMessage())], []];
50. }
51.
52. // Retrieve data from the tax authority
53. // first check the [redis] cache
54. if (!$redis->get("taxAdminData")) {
55. try {
56. // retrieve tax data from the database
57. $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
58. // taxAdminData
59. $taxAdminData = $dao->getTaxAdminData();
60. // store the retrieved data in Redis
61. $redis->set("taxAdminData", $taxAdminData);
62. } catch (\RuntimeException $ex) {
63. // Something went wrong
64. // return result with error to the main controller
65. $status = 1041;
66. return [Response::HTTP_INTERNAL_SERVER_ERROR, $status,
67. ["response" => utf8_encode($ex->getMessage())], []];
68. }
69. } else {
70. // tax data is retrieved from the [redis] store with [application] scope
71. $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
72. // instantiate a [TaxAdminData] object from the previous array of attributes
73. $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
74. }
75.
76. // Return the result to the main controller
77. $status = 1000;
78. return [Response::HTTP_OK, $status, ["response" => $taxAdminData], []];
79. }
80.
81. }
Comentarios
- línea 12: al igual que el resto de controladores del servidor, [AdminDataController] implementa el [InterfaceController] interfaz consistente en el [execute] método en las líneas 19-79;
- línea 78: al igual que ocurre con el resto de controladores del servidor, el [AdminDataController.execute] método devuelve un array [$estado, $estado, ['respuesta'=>$respuesta]] con:
- [$status]: el código de estado de la respuesta HTTP;
- [$état]: un código interno de la aplicación que representa el estado del servidor tras ejecutar la petición del cliente;
- [$respuesta]: un array que encapsula la respuesta que se enviará al cliente. Aquí, este array se convertirá posteriormente en una cadena JSON;
- líneas 25-34: comprobamos que la [get-admindata] acción del cliente es sintácticamente correcta;
- líneas 37-74: recuperar un [TaxAdminData] objeto encontrado bien:
- líneas 56-59: de la base de datos si no se encontró en la [redis] cache;
- líneas 70-73: en el [redis] cache;
Este código está tomado del [CalculerImpotController] controlador explicado en el artículo enlazado. De hecho, este controlador también necesitaba recuperar el [TaxAdminData] objeto que encapsula los datos de la administración tributaria.
Durante las pruebas del cliente JavaScript, el formato JSON de [TaxAdminData] provocaba problemas cuando este objeto se encontraba en la [redis] cache. Para entender por qué, examinemos cómo se almacena este objeto en [redis]:


- En [5-7], vemos que los valores numéricos se almacenaban como cadenas. PHP manejó esto porque el operador + en cálculos que involucran números y cadenas implícitamente causa una conversión de tipo de cadena a número. Pero JavaScript hace lo contrario: el operador + en los cálculos que implican números y cadenas provoca implícitamente una conversión de tipo de un número a una cadena. Por lo tanto, los cálculos de la clase [Métier] de JavaScript son incorrectos;
Para solucionar este problema, modificamos el [TaxAdminData.setFromArrayOfAttributes] método utilizado en la línea 71 del controlador para instanciar un [TaxAdminData] objeto (ver artículo) a partir de la cadena JSON que se encuentra en la [redis] cache:
1. <?php
2.
3. namespace Application;
4.
5. class TaxAdminData extends BaseEntity {
6. // tax brackets
7. protected $limits;
8. protected $coeffR;
9. protected $coeffN;
10. // tax calculation constants
11. protected $halfShareIncomeLimit;
12. protected $singleIncomeLimitForReduction;
13. protected $coupleIncomeLimitForReduction;
14. protected $half-share-reduction-value;
15. protected $singleDiscountCeiling;
16. protected $coupleDiscountLimit;
17. protected $coupleTaxCeilingForDiscount;
18. protected $singleTaxCeilingForDeduction;
19. protected $MaxTenPercentDeduction;
20. protected $minimumTenPercentDeduction;
21.
22. // initialization
23. public function setFromJsonFile(string $taxAdminDataFilename) {
24. // parent
25. parent::setFromJsonFile($taxAdminDataFilename);
26. // check attribute values
27. $this->checkAttributes();
28. // return the object
29. return $this;
30. }
31.
32. protected function check($value): \stdClass {
33. // $value is an array of string elements or a single element
34. if (!\is_array($value)) {
35. $array = [$value];
36. } else {
37. $array = $value;
38. }
39. // Convert the array of strings to an array of real numbers
40. $newArray = [];
41. $result = new \stdClass();
42. // the elements of the array must be positive or zero decimal numbers
43. $pattern = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
44. for ($i = 0; $i < count($array); $i++) {
45. if (preg_match($pattern, $array[$i])) {
46. // add the float to newArray
47. $newArray[] = (float) $array[$i];
48. } else {
49. // log the error
50. $result->error = TRUE;
51. // exit
52. return $result;
53. }
54. }
55. // return the result
56. $result->error = FALSE;
57. if (!\is_array($value)) {
58. // a single value
59. $result->value = $newArray[0];
60. } else {
61. // a list of values
62. $result->value = $newArray;
63. }
64. return $result;
65. }
66.
67. // initialization using an array of attributes
68. public function setFromArrayOfAttributes(array $arrayOfAttributes) {
69. // parent
70. parent::setFromArrayOfAttributes($arrayOfAttributes);
71. // check the attribute values
72. $this->checkAttributes();
73. // return the object
74. return $this;
75. }
76.
77. // Check attribute values
78. protected function checkAttributes() {
79. // check that attribute values are real numbers >= 0
80. foreach ($this as $key => $value) {
81. if (is_string($value)) {
82. // $value must be a real number >= 0 or an array of real numbers >= 0
83. $result = $this->check($value);
84. // error?
85. if ($result->error) {
86. // throw an exception
87. throw new ExceptionImpots("The value of the [$key] attribute is invalid");
88. } else {
89. // store the value
90. $this->$key = $result->value;
91. }
92. }
93. }
94.
95. // return the object
96. return $this;
97. }
98.
99. // getters and setters
100. ...
101.
102. }
Comentarios
- línea 5: la [TaxAdminData] clase extiende a la [BaseEntity] clase, que ya tiene el [setFromArrayOfAttributes] método. Como este método no es adecuado, lo redefinimos en las líneas 67-75;
- línea 70: el [setFromArrayOfAttributes] método de la clase padre se utiliza primero para inicializar los atributos de la clase;
- línea 72: el [checkAttributes] método verifica que los valores asociados son efectivamente números. Si son cadenas, se convierten en números;
- línea 74: el [$this] objeto resultante es entonces un objeto con atributos que tienen valores numéricos;
- líneas 78-93: el [checkAttributes] método verifica que los valores asociados a los atributos del objeto son efectivamente numéricos;
- línea 80: se recorre la lista de atributos;
- línea 81: si el valor de un atributo es de tipo [cadena];
- línea 83: entonces comprobamos que esta cadena representa un número;
- línea 90: si es así, la cadena se convierte en un número y se asigna al atributo que se está comprobando;
- líneas 85-86: si no, se lanza una excepción;
- líneas 32-65: la función [check] hace un poco más de lo necesario. Maneja tanto matrices como valores individuales. Sin embargo, aquí sólo se llama para comprobar un valor de tipo [cadena]. Devuelve un objeto con las propiedades [error, valor] where:
- [error] es un booleano que indica si se ha producido un error o no;
- [valor] es el [valor] parámetro de la línea 32, convertido a un número o a una matriz de números según corresponda;
La [BaseEntity] clase, que anteriormente tenía un atributo llamado [arrayOfAttributes], ha sido modificada para eliminar este atributo: estaba causando problemas con la [TaxAdminData] cadena JSON. La clase se ha reescrito de la siguiente manera:
1. <?php
2.
3. namespace Application;
4.
5. class BaseEntity {
6.
7. // initialization from a JSON file
8. public function setFromJsonFile(string $jsonFilename) {
9. // retrieve the contents of the tax data file
10. $fileContents = \file_get_contents($jsonFilename);
11. $error = FALSE;
12. // error?
13. if (!$fileContents) {
14. // log the error
15. $error = TRUE;
16. $message = "The data file [$jsonFilename] does not exist";
17. }
18. if (!$error) {
19. // retrieve the JSON code from the configuration file into an associative array
20. $arrayOfAttributes = \json_decode($fileContents, true);
21. // error?
22. if ($arrayOfAttributes === FALSE) {
23. // log the error
24. $error = TRUE;
25. $message = "The JSON data file [$jsonFilename] could not be processed correctly";
26. }
27. }
28. // error?
29. if ($error) {
30. // throw an exception
31. throw new TaxException($message);
32. }
33. // Initialize the class attributes
34. foreach ($arrayOfAttributes as $key => $value) {
35. $this->$key = $value;
36. }
37. // check for the presence of all attributes
38. $this->checkForAllAttributes($arrayOfAttributes);
39. // return the object
40. return $this;
41. }
42.
43. public function checkForAllAttributes($arrayOfAttributes) {
44. // Check that all keys have been initialized
45. foreach (\array_keys($arrayOfAttributes) as $key) {
46. if (!isset($this->$key)) {
47. throw new ExceptionImpots("The [$key] attribute of the class "
48. . get_class($this) . " has not been initialized");
49. }
50. }
51. }
52.
53. public function setFromArrayOfAttributes(array $arrayOfAttributes) {
54. // initialize some of the class's attributes (not necessarily all)
55. foreach ($arrayOfAttributes as $key => $value) {
56. $this->$key = $value;
57. }
58. // return the object
59. return $this;
60. }
61.
62. // toString
63. public function __toString() {
64. // object attributes
65. $arrayOfAttributes = \get_object_vars($this);
66. // JSON string of the object
67. return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
68. }
69.
70. }
Comentarios
- línea 20: el atributo [$this→arrayOfAttributes] se ha convertido en una variable que ahora debe pasarse al [checkForAllAttributes] método de la línea 38, que antes operaba sobre el atributo [$this→arrayOfAttributes];
Debido a este cambio en [BaseEntity], la clase [Database] también debe modificarse ligeramente:
1. <?php
2.
3. namespace Application;
4.
5. class Database extends BaseEntity {
6. // attributes
7. protected $dsn;
8. protected $id;
9. protected $pwd;
10. protected $tableTranches;
11. protected $colLimits;
12. protected $colCoeffR;
13. protected $colCoeffN;
14. protected $constantTable;
15. protected $colQfDemiPartLimit;
16. protected $colIncomeLimitSingleForDeduction;
17. protected $colIncomeLimitCoupleForReduction;
18. protected $colHalfShareReductionValue;
19. protected $colSingleDiscountLimit;
20. protected $colCoupleDiscountLimit;
21. protected $colIncomeCeilingSingleForDeduction;
22. protected $colCoupleTaxCeilingForDeduction;
23. protected $colMaxTenPercentDeduction;
24. protected $colMinTenPercentDeduction;
25.
26. // setter
27. // initialization
28. public function setFromJsonFile(string $jsonFilename) {
29. // parent
30. parent::setFromJsonFile($jsonFilename);
31. // return the object
32. return $this;
33. }
34.
35. // getters and setters
36. ...
37. }
Comentarios
- En el código original, después de la línea 30, se llamaba al método [parent::checkForAllAttributes] . Esto ya no es necesario puesto que ahora lo gestiona automáticamente el método [parent::setFromJsonFile($jsonFilename)];
14.3.4. [Postman] pruebas del servidor
[Cartero]se introdujo en el vinculado artículo.
Utilizamos las siguientes pruebas de Postman:



El resultado JSON de esta última petición es el siguiente:

- En [5-8], se puede ver que los atributos de la cadena JSON sí tienen valores numéricos (no cadenas). Este resultado permitirá que la clase JavaScript [Business] se ejecute normalmente;
14.3.5. El script principal [main]

El script principal [main] del cliente JavaScript es el siguiente:
1. // imports
2. import axios from 'axios';
3.
4. // imports
5. import Dao from './Dao2';
6. import Business from './Business';
7.
8. // asynchronous function [main]
9. async function main() {
10. // Axios configuration
11. axios.defaults.timeout = 2000;
12. axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
13. // Instantiate the [dao] layer
14. const dao = new Dao(axios);
15. // HTTP requests
16. let taxAdminData;
17. try {
18. // initialize session
19. log("-----------init-session");
20. let response = await dao.initSession();
21. log(response);
22. // authentication
23. log("-----------authenticate-user");
24. response = await dao.authenticateUser("admin", "admin");
25. log(response);
26. // tax data
27. log("-----------get-admindata");
28. response = await dao.getAdminData();
29. log(response);
30. taxAdminData = response.response;
31. } catch (error) {
32. // log the error
33. console.log("error=", error.message);
34. // end
35. return;
36. }
37.
38. // instantiate the [business] layer
39. const business = new Business(taxAdminData);
40.
41. // tax calculations
42. log("-----------calculate-tax x 3");
43. const simulations = [];
44. simulations.push(business.calculateTax("yes", 2, 45000));
45. simulations.push(job.calculateTax("no", 2, 45000));
46. simulations.push(job.calculateTax("no", 1, 30000));
47. // list of simulations
48. log("-----------list-of-simulations");
49. log(simulations);
50. // Delete a simulation
51. log("-----------deleting simulation #1");
52. simulations.splice(1, 1);
53. log(simulations);
54. }
55.
56. // JSON log
57. function log(object) {
58. console.log(JSON.stringify(object, null, 2));
59. }
60.
61. // execute
62. main();
Comentarios
- líneas 5-6: importaciones de las [Dao] y [Business] clases;
- línea 9: la función asíncrona [main] que se encargará de la comunicación con el servidor utilizando la [Dao] clase y pedirá a la [Business] clase que realice los cálculos fiscales;
- líneas 10-36: el script llama a los [initSession, authenticateUser, getAdminData] métodos de la [Dao] capa de forma secuencial y bloqueante;
- línea 38: ya no necesitamos la [dao] capa. Tenemos todos los elementos necesarios para ejecutar la [business] capa del cliente JavaScript;
- líneas 41-46: realizamos tres cálculos de impuestos y almacenamos los resultados en una matriz [simulaciones];
- línea 49: mostramos la matriz de simulaciones;
- línea 52: eliminamos uno de ellos;
Los resultados de la ejecución del script principal son los siguientes:
1. [Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 2\main2.js"
2. "-----------init-session"
3. {
4. "action": "init-session",
5. "status": 700,
6. "response": "session started with type [json]"
7. }
8. "-----------authenticate-user"
9. {
10. "action": "authenticate-user",
11. "status": 200,
12. "response": "Authentication successful [admin, admin]"
13. }
14. "-----------get-admindata"
15. {
16. "action": "get-admindata",
17. "status": 1000,
18. "response": {
19. "limits": [
20. 9964,
21. 27519,
22. 73779,
23. 156244,
24. 0
25. ],
26. "coeffR": [
27. 0,
28. 0.14,
29. 0.3,
30. 0.41,
31. 0.45
32. ],
33. "coeffN": [
34. 0,
35. 1394.96,
36. 5798,
37. 13913.69,
38. 20163.45
39. ],
40. "half-share-income-limit": 1551,
41. "IncomeLimitSingleForReduction": 21037,
42. "coupleIncomeLimitForReduction": 42074,
43. "half-share-reduction-value": 3797,
44. "singleDiscountLimit": 1196,
45. "coupleDiscountLimit": 1970,
46. "coupleTaxCeilingForDiscount": 2627,
47. "singleTaxCeilingForDiscount": 1595,
48. "MaxTenPercentDeduction": 12502,
49. "minimumTenPercentDeduction": 437
50. }
51. }
52. "-----------calculate-tax x 3"
53. "-----------list-of-simulations"
54. [
55. {
56. "tax": 502,
57. "surcharge": 0,
58. "discount": 857,
59. "discount": 126,
60. "rate": 0.14
61. },
62. {
63. "tax": 3250,
64. "surcharge": 370,
65. "discount": 0,
66. "discount": 0,
67. "rate": 0.3
68. },
69. {
70. "tax": 1687,
71. "surcharge": 0,
72. "discount": 0,
73. "reduction": 0,
74. "rate": 0.14
75. }
76. ]
77. "-----------simulation removal #1"
78. [
79. {
80. "tax": 502,
81. "surcharge": 0,
82. "discount": 857,
83. "discount": 126,
84. "rate": 0.14
85. },
86. {
87. "tax": 1687,
88. "surcharge": 0,
89. "discount": 0,
90. "discount": 0,
91. "rate": 0.14
92. }
93. ]
94.
95. [Done] exited with code=0 in 0.583 seconds
14.4. Cliente HTTP 3

En esta sección, portamos la [Cliente HTTP 2] aplicación a un navegador utilizando la siguiente arquitectura:

El proceso de portabilidad no es sencillo. Mientras que [node.js] puede ejecutar JavaScript ES6, generalmente no es el caso de los navegadores. Por lo tanto, debemos utilizar herramientas que traduzcan el código ES6 en código ES5 entendido por los navegadores modernos. Afortunadamente, estas herramientas son a la vez potente y bastante fácil de usar.
Aquí, seguimos el artículo [Cómo escribir código ES6 que sea seguro para ejecutarse en el navegador - Web Developer's Journal].
En la [cliente HTTP 3/src] carpeta, colocamos los [main.js, Métier.js, Dao2.js] archivos de la [Client Http 2] aplicación que acabamos de desarrollar.
14.4.1. Inicializando el proyecto
Trabajaremos en la [cliente http 3] carpeta. Abrimos un terminal en [VSCode] y navegamos hasta esta carpeta:

Inicializamos este proyecto con el [npm init] comando y aceptamos las respuestas por defecto a las preguntas planteadas:

- en [4-5], el fichero de configuración del proyecto [package.json] generado a partir de las distintas respuestas proporcionadas;
14.4.2. Instalando dependencias del proyecto
Instalaremos las siguientes dependencias:
- [@babel/core]: el núcleo de la [Babel] herramienta [https://babeljs.io], que transforma el código ES 2015+ en código ejecutable tanto en navegadores modernos como antiguos;
- [@babel/preset-env]: parte del conjunto de herramientas de Babel. Se ejecuta antes de la transpilación ES6 → ES5;
- [babel-loader]: esta dependencia permite que la [webpack] herramienta invoque a la [Babel] herramienta;
- [webpack]: el orquestador. Es [webpack] que invoca a Babel para transcompilar código ES6 a ES5, y luego ensambla todos los archivos resultantes en un único archivo;
- [webpack-cli]: necesario para [webpack];
- [@webpack-cli/init]: se utiliza para configurar [webpack];
- [webpack-dev-server]: proporciona un servidor web de desarrollo que se ejecuta por defecto en el puerto 8080. Cuando se modifican los archivos fuente, recarga automáticamente la aplicación web;
Las dependencias del proyecto se instalan de la siguiente manera en un [VSCode] terminal:
npm --save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

Después de instalar las dependencias, el [package.json] archivo ha cambiado como sigue:
1. {
2. "name": "client-http-3",
3. "version": "1.0.0",
4. "description": "JavaScript client for the tax calculation server",
5. "main": "index.js",
6. "scripts": {
7. "test": "echo \"Error: no test specified\" && exit 1"
8. },
9. "author": "serge.tahe@gmail.com",
10. "license": "ISC",
11. "devDependencies": {
12. "@babel/core": "^7.6.0",
13. "@babel/preset-env": "^7.6.0",
14. "@webpack-cli/init": "^0.2.2",
15. "babel-loader": "^8.0.6",
16. "cross-env": "^6.0.0",
17. "webpack": "^4.40.2",
18. "webpack-cli": "^3.3.9",
19. "webpack-dev-server": "^3.8.1"
20. }
21. }
- líneas 12-19: las dependencias del proyecto son [devDependencias]: las necesitamos durante la fase de desarrollo pero no en la de producción. En producción, se utiliza el fichero [dist/main.js] . Está escrito en ES5 y ya no requiere herramientas para transpilar código ES6 a ES5;
Necesitamos añadir dos dependencias al proyecto:
- [core-js]: contiene "polyfills" para ECMAScript 2019. Un polyfill permite que código reciente, como ECMAScript 2019 (septiembre de 2019), se ejecute en navegadores antiguos;
- [regenerator-runtime]: según el sitio web de la biblioteca --> [Transformador de código fuente que habilita las funciones del generador ECMAScript 6 en JavaScript-de-hoy];
A partir de Babel 7, estas dos dependencias sustituyen a la [@babel/polyfill] dependencia, que antes servía para este propósito y ahora (septiembre de 2019) está obsoleta. Se instalan de la siguiente manera:

El [package.json] archivo cambia entonces como sigue:
1. {
2. "name": "client-http-3",
3. "version": "1.0.0",
4. "description": "My webpack project",
5. "main": "index.js",
6. "scripts": {
7. "test": "echo \"Error: no test specified\" && exit 1",
8. "build": "webpack",
9. "start": "webpack-dev-server"
10. },
11. "author": "serge.tahe@gmail.com",
12. "license": "ISC",
13. "devDependencies": {
14. "@babel/core": "^7.6.0",
15. "@babel/preset-env": "^7.6.0",
16. "@webpack-cli/init": "^0.2.2",
17. "babel-loader": "^8.0.6",
18. "babel-plugin-syntax-dynamic-import": "^6.18.0",
19. "html-webpack-plugin": "^3.2.0",
20. "webpack": "^4.40.2",
21. "webpack-cli": "^3.3.9",
22. "webpack-dev-server": "^3.8.1"
23. },
24. "dependencies": {
25. "core-js": "^3.2.1",
26. "regenerator-runtime": "^0.13.3"
27. }
28. }
Usar las [core-js, regenerator-runtime] dependencias requiere añadir lo siguiente [imports] (líneas 3-4) al script principal [src/main.js]:
1. // imports
2. import axios from 'axios';
3. import "core-js/stable";
4. import "regenerator-runtime/runtime";
5.
6. // imports
7. import Dao from './Dao2';
8. import Business from './Business';
14.4.3. [webpack] configuración
[webpack] es la herramienta que se encargará de:
- la transpilación de todos los archivos JavaScript del proyecto de ES6 a ES5;
- la agrupación de los archivos generados en un único archivo;
Esta herramienta está controlada por un archivo de configuración [webpack.config.js], que se puede generar mediante una dependencia llamada [@webpack-cli/init] (Sept. 2019). Esta dependencia se instaló junto con las demás mencionadas en la sección enlace.
Ejecutamos el comando [npx webpack-cli init] en un [VSCode] terminal:

Tras responder a las distintas preguntas (para las que podemos aceptar la mayoría de las respuestas por defecto), se genera un [webpack.config.js] archivo en la raíz del proyecto [4]:
El [webpack.config.js] archivo tiene este aspecto:
1. /* eslint-disable */
2.
3. const path = require('path');
4. const webpack = require('webpack');
5.
6. /*
7. * SplitChunksPlugin is enabled by default and has been replaced
8. * the deprecated CommonsChunkPlugin. It automatically identifies modules that
9. * should be split into chunks based on heuristics using the number of module duplicates and
10. * module category (e.g., node_modules). And splits the chunks…
11. *
12. * It is safe to remove "splitChunks" from the generated configuration
13. * and was added as an educational example.
14. *
15. * https://webpack.js.org/plugins/split-chunks-plugin/
16. *
17. */
18.
19. const HtmlWebpackPlugin = require('html-webpack-plugin');
20.
21. /*
22. * We've enabled HtmlWebpackPlugin for you! This generates an HTML
23. * page for you when you compile webpack, which will help you start
24. * developing and prototyping faster.
25. *
26. * https://github.com/jantimon/html-webpack-plugin
27. *
28. */
29.
30. module.exports = {
31. mode: 'development',
32. entry: './src/index.js',
33.
34. output: {
35. filename: '[name].[chunkhash].js',
36. path: path.resolve(__dirname, 'dist')
37. },
38.
39. plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
40.
41. module: {
42. rules: [
43. {
44. test: /.(js|jsx)$/,
45. include: [path.resolve(__dirname, 'src')],
46. loader: 'babel-loader',
47.
48. options: {
49. plugins: ['syntax-dynamic-import'],
50.
51. presets: [
52. [
53. '@babel/preset-env',
54. {
55. modules: false
56. }
57. ]
58. ]
59. }
60. }
61. ]
62. },
63.
64. optimization: {
65. splitChunks: {
66. cacheGroups: {
67. vendors: {
68. priority: -10,
69. test: /[\\/]node_modules[\\/]/
70. }
71. },
72.
73. chunks: 'async',
74. minChunks: 1,
75. minSize: 30000,
76. name: true
77. }
78. },
79.
80. devServer: {
81. open: true
82. }
83. };
No entiendo todos los detalles de este archivo, pero algunas cosas llaman la atención:
- línea 1: el archivo no contiene código ES6. [Eslint] luego informa de errores que se propagan hasta la raíz del [javascript] proyecto. Esto es molesto. Para evitar que Eslint analice un archivo, simplemente comente la línea 1;
- línea 31: estamos trabajando en [desarrollo] modo;
- línea 32: el script de entrada está aquí [src/index.js]. Tendremos que cambiarlo;
- línea 36: la carpeta donde se colocará la salida de [webpack] es la [dist] carpeta;
- línea 46: podemos ver que [webpack] utiliza [babel-loader], una de las dependencias que instalamos;
- línea 54: vemos que [webpack] utiliza [@babel-preset/env], una de las dependencias que instalamos;
Inicializando [webpack] ha modificado el [package.json] archivo (pide permiso):
1. {
2. "name": "client-http-3",
3. "version": "1.0.0",
4. "description": "My webpack project",
5. "main": "index.js",
6. "scripts": {
7. "test": "echo \"Error: no test specified\" && exit 1",
8. "build": "webpack",
9. "start": "webpack-dev-server"
10. },
11. "author": "serge.tahe@gmail.com",
12. "license": "ISC",
13. "devDependencies": {
14. "@babel/core": "^7.6.0",
15. "@babel/preset-env": "^7.6.0",
16. "@webpack-cli/init": "^0.2.2",
17. "babel-loader": "^8.0.6",
18. "babel-plugin-syntax-dynamic-import": "^6.18.0",
19. "html-webpack-plugin": "^3.2.0",
20. "webpack": "^4.40.2",
21. "webpack-cli": "^3.3.9",
22. "webpack-dev-server": "^3.8.1"
23. },
24. "dependencies": {
25. "core-js": "^3.2.1",
26. "regenerator-runtime": "^0.13.3"
27. }
28. }
- línea 4: se ha modificado;
- líneas 8-9, 18-19: se han añadido;
- línea 8: la [npm] tarea que compila el proyecto;
- línea 9: la [npm] tarea que lo ejecuta;
- línea 18: ?
- línea 19: genera un [dist/index.html] archivo que incrusta automáticamente el [dist/main.js] script generado por [webpack], y este es el que se utiliza al ejecutar el proyecto;
Finalmente, la [webpack] configuración generó un archivo [src/index.js]:

El contenido de [index.js] es el siguiente (septiembre de 2019):
1. console.log("Hello World from your main file!");
14.4.4. Compilar y ejecutar el proyecto
El [package.json] archivo tiene tres [npm] tareas:
1. "scripts": {
2. "test": "echo \"Error: no test specified\" && exit 1",
3. "build": "webpack",
4. "start": "webpack-dev-server"
5. },
Estas tareas son reconocidas por [VSCode], que las ofrece para su ejecución:

- en [1-3], se compila el proyecto;
- en [4]: se compila el proyecto en [dist/main.hash.js] y se crea una [dist/index.html] página;
La página generada [index.html] es la siguiente:
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <meta charset="UTF-8">
5. <title>Webpack App</title>
6. </head>
7. <body>
8. <script type="text/javascript" src="main.87afc226fd6d648e7dea.js"></script></body>
9. </html>
Esta página simplemente encapsula el [main.hash.js] archivo generado por [webpack].
El proyecto es ejecutado por la [start] tarea:

A continuación, la [dist/index.html] página se carga en un servidor, parte del [webpack] suite, ejecutándose en el puerto 8080 de la máquina local y mostrándose en el navegador por defecto de la máquina:

- en [2], el puerto de servicio del [webpack] servidor web;
- en [3], el cuerpo de la [dist/index.html] página está vacío;
- en [4], la [consola] pestaña de las herramientas para desarrolladores del navegador, aquí Firefox (F12);
- en [5], el resultado de ejecutar el fichero [src/index.js] . Recordemos que su contenido era el siguiente:
Ahora, cambiemos este contenido por la siguiente línea:
Automáticamente (sin recompilar), se generan nuevos archivos [main.js, index.html] y se carga el nuevo archivo [index.html] en el navegador:

No es necesario ejecutar la [build] tarea antes que la [start] tarea: esta última compila primero el proyecto. No almacena el resultado de esta compilación en la carpeta [dist] . Para comprobarlo, basta con borrar esta carpeta. Veremos entonces que la [start] tarea compila y ejecuta el proyecto sin crear la [dist] carpeta. Parece almacenar su salida [index.html, main.hash.js] en una carpeta específica de [webpackdev-server]. Este comportamiento es suficiente para nuestras pruebas.
Cuando el servidor de desarrollo se está ejecutando, cualquier cambio guardado en un archivo de proyecto desencadena una recompilación. Por esta razón, desactivamos [VSCode]del [Auto Save] modo. No queremos que el proyecto se recompile cada vez que escribimos caracteres en un archivo de proyecto. Sólo queremos que la recompilación se produzca cuando se guarden los cambios:

- en [2], la opción [Guardar automáticamente] no debe estar marcada;
14.4.5. Probando el cliente JavaScript para el servidor de cálculo de impuestos
Para probar el cliente JavaScript para el servidor de cálculo de impuestos, debe designar [main.js] [1] como punto de entrada del proyecto en el [webpack.config.js] archivo [2-3]:

Recuerda que el [main.js] script debe incluir dos importaciones adicionales respecto a su versión en [HTTP Client 2]:

Además, hemos modificado ligeramente el código para gestionar los errores que pueda devolver el servidor:
1. // imports
2. import axios from 'axios';
3. import "core-js/stable";
4. import "regenerator-runtime/runtime";
5.
6. // imports
7. import Dao from './Dao2';
8. import Business from './Business';
9.
10. // asynchronous function [main]
11. async function main() {
12. // Axios configuration
13. axios.defaults.timeout = 2000;
14. axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
15. // instantiate the [dao] layer
16. const dao = new Dao(axios);
17. // HTTP requests
18. let taxAdminData;
19. try {
20. // initialize session
21. log("-----------init-session");
22. let response = await dao.initSession();
23. log(response);
24. if (response.status != 700) {
25. throw new Error(JSON.stringify(response.response));
26. }
27. // authentication
28. log("-----------authenticate-user");
29. response = await dao.authenticateUser("admin", "admin");
30. log(response);
31. if (response.status != 200) {
32. throw new Error(JSON.stringify(response.response));
33. }
34. // tax data
35. log("-----------get-admindata");
36. response = await dao.getAdminData();
37. log(response);
38. if (response.status != 1000) {
39. throw new Error(JSON.stringify(response.response));
40. }
41. taxAdminData = response.response;
42. } catch (error) {
43. // log the error
44. console.log("error=", error.message);
45. // end
46. return;
47. }
48.
49. // Instantiate the [business] layer
50. const business = new Business(taxAdminData);
51.
52. // tax calculations
53. log("-----------calculate-tax x 3");
54. const simulations = [];
55. simulations.push(business.calculateTax("yes", 2, 45000));
56. simulations.push(job.calculateTax("no", 2, 45000));
57. simulations.push(job.calculateTax("no", 1, 30000));
58. // list of simulations
59. log("-----------list-of-simulations");
60. log(simulations);
61. // Delete a simulation
62. log("-----------deleting simulation #1");
63. simulations.splice(1, 1);
64. log(simulations);
65. }
66.
67. // JSON log
68. function log(object) {
69. console.log(JSON.stringify(object, null, 2));
70. }
71.
72. // execution
73. main();
Comentarios
- En las líneas [24-26], [31-33], [38-40], comprobamos el código [response.status] enviado en la respuesta JSON del servidor. Si este código indica un error, se lanza una excepción con la cadena JSON de la respuesta del servidor [response.response] como mensaje de error;
Una vez hecho esto, ejecutamos el proyecto [5-6].
A continuación, se genera la página [index.html] y se carga en el navegador:

- En [7], vemos que la [init-session] acción no ha podido completarse debido a un [CORS] (Cross-Origin Resource Sharing) problema;
El problema de CORS proviene de la relación cliente/servidor:
- Se ha descargado nuestro cliente JavaScript en la máquina [http://localhost:8080];
- el servidor de cálculo de impuestos se ejecuta en la máquina [http://localhost:80];
- por tanto, el cliente y el servidor no están en el mismo dominio (misma máquina pero puertos diferentes);
- el navegador que ejecuta el cliente JavaScript cargado desde la máquina [http://localhost:8080] bloquea cualquier petición que no tenga como objetivo [http://localhost:80]. Se trata de una medida de seguridad. Por tanto, bloquea la petición del cliente al servidor que se ejecuta en la máquina [http://localhost:80];
De hecho, el navegador no bloquea completamente la petición. En realidad espera a que el servidor le "diga" que acepta peticiones entre dominios. Si recibe esta autorización, el navegador transmitirá la petición entre dominios.
El servidor concede su autorización mediante el envío de cabeceras HTTP específicas:
1. Access-Control-Allow-Origin: http://localhost:8080
2. Access-Control-Allow-Headers: Accept, Content-Type
3. Access-Control-Allow-Methods: GET, POST
4. Access-Control-Allow-Credentials: true
- Línea 1: El cliente JavaScript opera en el dominio [http://localhost:8080]. El servidor debe responder explícitamente que acepta este dominio;
- Línea 2: El cliente JavaScript utilizará las cabeceras HTTP [Accept, Content-Type] en sus peticiones:
- [Accept]: esta cabecera se envía en cada solicitud;
- [Content-Type]: Esta cabecera se utiliza en operaciones POST para especificar el tipo de los parámetros POST;
El servidor debe aceptar explícitamente estas dos cabeceras HTTP;
- Línea 3: El cliente JavaScript utilizará peticiones GET y POST. El servidor debe aceptar explícitamente estos dos tipos de peticiones;
- Línea 4: El cliente JavaScript enviará cookies de sesión. El servidor las acepta con la cabecera de la línea 4;
Por lo tanto, tenemos que modificar el servidor. Esto lo hacemos en [NetBeans]. El problema de CORS se encuentra sólo en el modo de desarrollo. En producción, el cliente y el servidor operarán dentro del mismo dominio [http://localhost:80] y no habrá problemas de CORS. Por lo tanto, necesitamos una forma de habilitar o deshabilitar las peticiones CORS a través de la configuración del servidor.

Las modificaciones del servidor se realizan en tres lugares:
- [1, 4]: en el archivo de configuración [config.json] para establecer un booleano que controle si se aceptan o no solicitudes entre dominios;
- [2]: en la clase [ParentResponse] , que envía la respuesta al cliente JavaScript. Esta clase enviará las cabeceras CORS esperadas por el navegador cliente;
- [3]: en las [HtmlResponse, JsonResponse, XmlResponse] clases que generan respuestas para las [html, json, xml] sesiones, respectivamente. Estas clases deben pasar el [corsAllowed] booleano que se encuentra en [4] a su clase padre [2]. Esto se hace en [5], pasando el array de imágenes del fichero JSON [2];
La [ParentResponse] clase [2] evoluciona de la siguiente manera:
1. <?php
2.
3. namespace Application;
4.
5. // Symfony dependencies
6. use Symfony\Component\HttpFoundation\Response;
7. use Symfony\Component\HttpFoundation\Request;
8.
9. class ParentResponse {
10.
11. // int $statusCode: the HTTP status code of the response
12. // string $content: the body of the response to be sent
13. // depending on the case, this is a JSON, XML, or HTML string
14. // array $headers: the HTTP headers to add to the response
15.
16. public function sendResponse(
17. Request $request,
18. int $statusCode,
19. string $content,
20. array $headers,
21. array $config): void {
22.
23. // Prepare the server's text response
24. $response = new Response();
25. $response->setCharset("utf-8");
26. // status code
27. $response->setStatusCode($statusCode);
28. // headers for cross-domain requests
29. if ($config['corsAllowed']) {
30. $origin = $request->headers->get("origin");
31. if (strpos($origin, "http://localhost") === 0) {
32. $headers = array_merge($headers,
33. ["Access-Control-Allow-Origin" => $origin,
34. "Access-Control-Allow-Headers" => "Accept, Content-Type",
35. "Access-Control-Allow-Methods" => "GET, POST",
36. "Access-Control-Allow-Credentials" => "true"
37. ]);
38. }
39. }
40. foreach ($headers as $text => $value) {
41. $response->headers->set($text, $value);
42. }
43. // Special case of the [OPTIONS] method
44. // only the headers are important in this case
45. $method = strtolower($request->getMethod());
46. if ($method === "options") {
47. $content = "";
48. $response->setStatusCode(Response::HTTP_OK);
49. }
50. // send the response
51. $response->setContent($content);
52. $response->send();
53. }
54.
55. }
- línea 29: comprobamos si necesitamos gestionar peticiones cross-domain. Si es así, generamos las cabeceras HTTP CORS (líneas 33-37) incluso si la petición actual no es una petición cross-domain. En este último caso, las cabeceras CORS serán innecesarias y no serán utilizadas por el cliente;
- línea 30: en una petición entre dominios, el navegador cliente que consulta al servidor envía una cabecera HTTP [Origen: http://localhost:8080] (en el caso concreto de nuestro cliente JavaScript). En la línea 30, recuperamos esta cabecera HTTP de la petición [$petición];
- línea 31: sólo aceptaremos peticiones cross-domain originadas en la máquina [http://localhost]. Ten en cuenta que estas peticiones sólo se producen en el modo de desarrollo del proyecto;
- Líneas 32-36: Añadimos las cabeceras CORS a las cabeceras ya presentes en el array [$cabeceras];
- Líneas 45-49: La forma en que el navegador cliente solicita los permisos CORS puede variar en función del navegador utilizado. A veces el navegador cliente solicita estos permisos utilizando una [OPTIONS] petición HTTP. Este es un nuevo escenario para nuestro servidor, que fue construido para manejar sólo [GET] y [POST] solicitudes. En el caso de una [OPTIONS] petición, el servidor genera actualmente una respuesta de error. Líneas 46-49: corregimos esto en el último momento: si, en la línea 46, determinamos que la petición actual es una [OPTIONS] petición, entonces generamos lo siguiente para el cliente:
- líneas 47, 51: un [$content] vacío respuesta;
- línea 48: un código de estado 200 que indica que la petición se ha realizado correctamente. Lo único importante para esta petición es enviar las cabeceras CORS en las líneas 33-36. Esto es lo que espera el navegador del cliente;
Una vez corregido el servidor de esta forma, el cliente JavaScript funciona mejor pero muestra un nuevo error:

- en [1], la sesión JSON se inicializa correctamente;
- En [2], la [authenticate-user] acción falla: el servidor indica que no hay sesión activa. Esto significa que el cliente JavaScript no ha devuelto correctamente la cookie de sesión que envió durante la [init-session] acción;
Examinemos los intercambios de red que han tenido lugar:

- en [4], la [init-session] petición. Se ha completado correctamente con un código de estado 200 para la respuesta;
- en [5], la [authenticate-user] petición. Esta petición falla con un código de estado 400 (Bad Request) [6] para la respuesta;
Si examinamos las cabeceras HTTP [7] de la petición [5], podemos ver que el cliente JavaScript no envió la cabecera HTTP [Cookie], que le habría permitido devolver la cookie de sesión enviada inicialmente por el servidor. Por eso el servidor informa de que no hay sesión.
Para que el cliente envíe la cookie de sesión, es necesario añadir una configuración al [axios] objeto:
1. // imports
2. import axios from 'axios';
3. import "core-js/stable";
4. import "regenerator-runtime/runtime";
5.
6. // imports
7. import Dao from './Dao2';
8. import Business from './Business';
9.
10. // asynchronous function [main]
11. async function main() {
12. // Axios configuration
13. axios.defaults.timeout = 2000;
14. axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
15. axios.defaults.withCredentials = true;
16. // instantiate the [dao] layer
17. const dao = new Dao(axios);
18. // HTTP requests
19. let taxAdminData;
20. ...
La línea 15 requiere que se incluyan cookies en las cabeceras HTTP de la [axios] petición. Nótese que esto no era necesario en el [node.js] entorno. Existen, por tanto, diferencias de código entre ambos entornos.
Una vez solucionado este error, el cliente JavaScript se ejecuta con normalidad:


14.5. Mejora del cliente HTTP 3
Cuando la clase [Dao2] anterior se ejecuta dentro de un navegador, la gestión de la cookie de sesión es innecesaria. Esto se debe a que el navegador que aloja la capa [dao] gestiona la cookie de sesión: devuelve automáticamente cualquier cookie que el servidor le envíe. En consecuencia, la clase [Dao2] puede reescribirse como la siguiente clase [Dao3]:
1. "use strict";
2.
3. // imports
4. import qs from "qs";
5.
6. class Dao3 {
7. // constructor
8. constructor(axios) {
9. this.axios = axios;
10. }
11.
12. // initialize session
13. async initSession() {
14. // HTTP request options [get /main.php?action=init-session&type=json]
15. const options = {
16. method: "GET",
17. // URL parameters
18. params: {
19. action: "init-session",
20. type: "json"
21. }
22. };
23. // Execute the HTTP request
24. return await this.getRemoteData(options);
25. }
26.
27. async authenticateUser(user, password) {
28. // HTTP request options [post /main.php?action=authenticate-user]
29. const options = {
30. method: "POST",
31. headers: {
32. "Content-type": "application/x-www-form-urlencoded"
33. },
34. // POST body
35. data: qs.stringify({
36. user: user,
37. password: password
38. }),
39. // URL parameters
40. params: {
41. action: "authenticate-user"
42. }
43. };
44. // execute the HTTP request
45. return await this.getRemoteData(options);
46. }
47.
48. async getAdminData() {
49. // HTTP request options [get /main.php?action=get-admindata]
50. const options = {
51. method: "GET",
52. // URL parameters
53. params: {
54. action: "get-admindata"
55. }
56. };
57. // execute the HTTP request
58. const data = await this.getRemoteData(options);
59. // result
60. return data;
61. }
62.
63. async getRemoteData(options) {
64. // Execute the HTTP request
65. let response;
66. try {
67. // asynchronous request
68. response = await this.axios.request("main.php", options);
69. } catch (error) {
70. // The [error] parameter is an exception instance—it can take various forms
71. if (error.response) {
72. // the server's response is in [error.response]
73. response = error.response;
74. } else {
75. // we re-throw the error
76. throw error;
77. }
78. }
79. // response is the entire HTTP response from the server (HTTP headers + the response itself)
80. // The server's response is in [response.data]
81. return response.data;
82. }
83. }
84.
85. // export the class
86. export default Dao3;
Todo lo relacionado con la gestión de la cookie de gestión ha desaparecido.
Modificamos el proyecto anterior de la siguiente manera:

En la carpeta [src], hemos añadido dos archivos:
- la clase [Dao3] que acabamos de introducir;
- el archivo [main3] responsable de lanzar la nueva versión;
El archivo [main3] sigue siendo idéntico al archivo [main] de la versión anterior, pero ahora utiliza la clase [Dao3]
1. // imports
2. import axios from "axios";
3. import "core-js/stable";
4. import "regenerator-runtime/runtime";
5.
6. // imports
7. import Dao from "./Dao3";
8. import Business from "./Business";
9.
10. // asynchronous function [main]
11. async function main() {
12. // Axios configuration
13. axios.defaults.timeout = 2000;
14. axios.defaults.baseURL =
15. "http://localhost/php7/scripts-web/impots/version-14";
16. axios.defaults.withCredentials = true;
17. // instantiate the [dao] layer
18. const dao = new Dao(axios);
19. // HTTP requests
20. ...
21. }
22.
23. // JSON log
24. function log(object) {
25. console.log(JSON.stringify(object, null, 2));
26. }
27.
28. // execution
29. main();
El archivo [webpack.config] se modifica para ejecutar ahora el script [main3]
1. /* eslint-disable */
2.
3. const path = require("path");
4. const webpack = require("webpack");
5.
6. /*
7. * SplitChunksPlugin is enabled by default and has replaced
8. * the deprecated CommonsChunkPlugin. It automatically identifies modules that
9. * should be split into chunks based on heuristics using the number of module duplicates and
10. * module category (e.g., node_modules). And splits the chunks…
11. *
12. * It is safe to remove "splitChunks" from the generated configuration
13. * and was added as an educational example.
14. *
15. * https://webpack.js.org/plugins/split-chunks-plugin/
16. *
17. */
18.
19. const HtmlWebpackPlugin = require("html-webpack-plugin");
20.
21. /*
22. * We've enabled HtmlWebpackPlugin for you! This generates an HTML
23. * page for you when you compile webpack, which will help you start
24. * developing and prototyping faster.
25. *
26. * https://github.com/jantimon/html-webpack-plugin
27. *
28. */
29.
30. module.exports = {
31. mode: "development",
32. //entry: "./src/mainjs",
33. entry: "./src/main3.js",
34. output: {
35. filename: "[name].[chunkhash].js",
36. path: path.resolve(__dirname, "dist")
37. },
38.
39. plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
40. ...
41. };
Una vez hecho esto, ejecutamos el proyecto tras iniciar el servidor de cálculo de impuestos:

Los resultados mostrados en la consola del navegador son idénticos a los de la versión anterior.
14.6. Conclusión
Ahora tenemos todas las herramientas necesarias para desarrollar código JavaScript para una aplicación web. Podemos:
- utiliza el último código ECMAScript;
- prueba partes aisladas de este código en un [node.js] entorno, que es más sencillo para depurar y probar;
- luego porta este código a un navegador usando [babel] y [webpack] tools;