11. IMPOTS Ejercicio con un Servicio Web y una Arquitectura de Tres Niveles
Revisaremos el ejercicio IMPOTS (véanse las secciones 4.2, 4.3, 6) y convertirlo en una aplicación cliente/servidor. El script del servidor se dividirá en tres componentes:
- una capa denominada [DAO] (Objetos de acceso a datos) que se encargará de las interacciones con la base de datos MySQL
- una capa llamada [business] que calculará el impuesto
- una capa [web] que se encargará de la comunicación con los clientes web.
![]() |
El script cliente [1]:
- envía al script del servidor los tres datos ($casado, $hijos, $salario) necesarios para calcular el impuesto
- muestra la respuesta del servidor en la consola
El script del servidor [2] consiste en la capa [web] del servidor.
- Al inicio de una nueva sesión de cliente, rellenará las matrices con datos de la base de datos MySQL [dbimpots]. Para ello, recurrirá a la capa [DAO]. Las matrices así construidas se colocarán en la sesión del cliente para que puedan ser utilizadas en posteriores peticiones del cliente.
- Cuando un cliente realiza una solicitud, pasará los tres datos ($casado, $hijos, $salario) a la capa [lógica de negocio], que calculará el impuesto $impuesto.
- El script del servidor devolverá el impuesto calculado $tax.
11.1. El script cliente (clients_impots_05_web)
El script cliente será un cliente del servicio web de cálculo de impuestos. Enviará parámetros (POST) al servidor en el siguiente formato:
params=$married,$children,$salary where
- $casado será la cadena "sí o "no",
- niños será el número de hijos,
- $salario es el salario del contribuyente
Recupera los tres parámetros anteriores de un fichero de texto [data.txt] con el formato (casado, hijos, salario):
El script cliente
- leerá el archivo de texto [data.txt] línea por línea
- enviará la cadena params=$casado,$hijos,$salario al servicio web de cálculo de impuestos
- recuperar la respuesta del servicio. Esta respuesta puede adoptar dos formas:
- guardará la respuesta del servidor en un archivo de texto [results.txt] en uno de los dos formatos siguientes:
El código del script del lado del cliente es el siguiente:
1. <?php
2.
3. // tax client
4. // error handling
5. ini_set("display_errors", "off");
6.
7. // ---------------------------------------------------------------------------------
8. // a class of utility functions
9. class Utilities {
10.
11. function cutNewLinechar($line) {
12. ...
13. }
14.
15. }
16.
17. // main -----------------------------------------------------
18. // definition of constants
19. $DATA = "data.txt";
20. $RESULTS = "results.txt";
21. // server data
22. $HOST = "localhost";
23. $PORT = 80;
24. $serverUrl = "/web-examples/taxes_05_web.php";
25.
26. // Taxable individuals' parameters (marital status, number of children, annual income)
27. // have been placed in the text file $DATA, with one line per taxpayer
28. // the results (marital status, number of children, annual income, tax due)
29. // or (marital status, number of children, annual income, error message) are placed in
30. // the text file $RESULTS, with one result per line
31.
32. // Utilities class
33. $u = new Utilities();
34.
35. // open the taxpayer data file
36. $data = fopen($DATA, "r");
37. if (!$data) {
38. print "Unable to open the data file [$DATA] for reading\n";
39. exit;
40. }
41.
42. // Open the results file
43. $results = fopen($RESULTS, "w");
44. if (!$results) {
45. print "Unable to create the results file [$RESULTS]\n";
46. exit;
47. }
48.
49. // process the current line of the taxpayer data file
50. while ($line = fgets($data, 100)) {
51. // remove any end-of-line characters
52. $line = $u->cutNewLineChar($line);
53. // retrieve the 3 fields married:children:salary that make up $line
54. list($married, $children, $salary) = explode(",", $line);
55. // calculate the tax
56. list($error, $tax) = calculateTax($HOST, $PORT, $serverUrl, $cookie, array($married, $children, $salary));
57. // display the result
58. $result = $error ? "$married:$children:$salary:$error" : "$married:$children:$salary:$tax";
59. fputs($results, "$result\n");
60. // next data
61. }
62. // close the files
63. fclose($data);
64. fclose($results);
65.
66. // end
67. print "Done...\n";
68. exit;
69.
70. function calculateTax($HOST, $PORT, $serverUrl, &$cookie, $params) {
71. // connects the client to ($HOST, $PORT, $serverURL)
72. // sends the $cookie if it is not empty. $cookie is passed by reference
73. // sends $params to the server
74. // processes the single line returned by the server
75.
76. // Open a connection on port 80 of $HOST
77. $connection = fsockopen($HOST, $PORT);
78. // error?
79. if (!$connection)
80. return array("Error connecting to the server ($HOST, $PORT)");
81. // HTTP headers must end with an empty line
82. // POST
83. fputs($connection, "POST $serverURL HTTP/1.1\n");
84. // Host
85. fputs($connection, "Host: localhost\n");
86. // Connection
87. fputs($connection, "Connection: close\n");
88. // send the cookie if it is not empty
89. if ($cookie) {
90. fputs($connection, "Cookie: $cookie\n");
91. }//if
92. // now send the client instruction after encoding it
93. $infos = "params=" . urlencode(implode(",", $params));
94. // Specify the type of data to be sent
95. fputs($connection, "Content-type: application/x-www-form-urlencoded\n");
96. // send the size (number of characters) of the data to be sent
97. fputs($connection, "Content-length: " . strlen($info) . "\n");
98. // send an empty line
99. fputs($connection, "\n");
100. // send the data
101. fputs($connection, $data);
102. // display the web server's response
103. // and make sure to retrieve any cookies
104. while ($line = fgets($connection, 1000)) {
105. // cookie - only on the first response
106. if (!$cookie) {
107. if (preg_match("/^Set-Cookie: (.*?)\s*$/", $line, $fields)) {
108. $cookie = $fields[1];
109. }//if
110. }
111. // as soon as there is an empty line, the HTTP response is complete
112. if (trim($line) == "") {
113. break;
114. }
115. }//while
116. // read a line of the result
117. $line = fgets($connection, 1000);
118. // Close the connection
119. fclose($connection);
120. // calculate result
121. $error="";
122. $tax = "";
123. if (preg_match("/^<error>(.*?)<\/error>\s*$/", $line, $fields)) {
124. $error = $fields[1];
125. } else {
126. if (preg_match("/^<tax>(.*?)<\/tax>\s*$/", $line, $fields)) {
127. $tax = $fields[1];
128. } else {
129. $error = "unusable server result";
130. }
131. }
132. // return
133. return array($error, $tax);
134. }
Comentarios
El código script del lado del cliente incluye elementos que ya hemos visto antes:
- líneas 9-15: la clase [Utilities] se introdujo en la versión 3, sección 6
- líneas 17-68: el programa principal es similar al de la Versión 1, Sección 4.2. Sólo difiere en el cálculo del impuesto, línea 56.
- línea 56: la función de cálculo de impuestos acepta los siguientes parámetros:
- $HOST, $PORT, $serverURL: utilizado para conectarse al servicio web
- $cookie: es la cookie de sesión. Este parámetro se pasa por referencia. Su valor lo establece la función de cálculo de impuestos. En la primera llamada, no tiene valor. Después, tiene uno.
- array($married, $children, $salary): represents a row from the [data.txt] file
La función de cálculo de impuestos devuelve una matriz de dos resultados ($error, $tax) donde $error es un posible mensaje de error y $tax es el importe del impuesto.
- Líneas 70-134: Este es un cliente HTTP clásico, del tipo que hemos encontrado muchas veces. Fíjate en los siguientes puntos:
- Línea 83: Los parámetros ($married, $children, $salary) se envían al servidor mediante una petición POST
- Líneas 89-91: Si el cliente tiene una sesión ID, la envía al servidor
- línea 93: creación del parámetros parámetro
- línea 101: el `parámetros se envía el parámetro
- líneas 104-115: el cliente lee todas las cabeceras HTTP enviadas por el servidor hasta que encuentra la línea vacía que marca el final de las cabeceras. Aprovecha esta oportunidad para recuperar la sesión ID de la respuesta a su primera petición.
- líneas 123-125: procesa cualquier línea del formulario <error>mensaje</error>
- líneas 126-128: hacemos lo mismo con cualquier línea de la forma <tax>importe</tax>
- línea 133: se devuelve el resultado
11.2. El servicio web de cálculo de impuestos
Aquí nos interesan los tres scripts que componen el servidor:
![]() |
El proyecto NetBeans correspondiente es el siguiente:
![]() |
En [1], el servidor consta de los siguientes scripts PHP:
- [impots_05_entites] contiene las clases utilizadas por el servidor
- [impots_05_dao] contiene las clases e interfaces de la capa [dao]
- [impots_05_metier] contiene las clases e interfaces de la capa [business]
- [impots_05_web] contiene las clases e interfaces de la capa [dao]
Comenzamos presentando dos clases utilizadas por las diferentes capas del servicio web.
11.2.1. Las entidades del servicio web (impots_05_entities)
La base de datos MySQL [dbimpots] tiene una tabla [impots] que contiene los datos necesarios para calcular el impuesto [1]:
![]() |
Almacenaremos los datos de la tabla MySQL [impots] en un array de Tramo objetos, donde Tramo es la siguiente clase:
1. <?php
2.
3. // a tax bracket
4. class Tranche {
5.
6. // private fields
7. private $limit;
8. private $coeffR;
9. private $coeffN;
10.
11. // getters and setters
12. public function getLimit() {
13. return $this->limit;
14. }
15.
16. public function setLimit($limit) {
17. $this->limit = $limit;
18. }
19.
20. public function getRate() {
21. return $this->coeffR;
22. }
23.
24. public function setCoeffR($coeffR) {
25. $this->coeffR = $coeffR;
26. }
27.
28. public function getCoeffN() {
29. return $this->coeffN;
30. }
31.
32. public function setCoeffN($coeffN) {
33. $this->coeffN = $coeffN;
34. }
35.
36. // constructor
37. public function __construct($limit, $coeffR, $coeffN) {
38. $this->setLimit($limit);
39. $this->setCoeffR($coeffR);
40. $this->setCoeffN($coeffN);
41. }
42.
43. // toString
44. public function __toString(){
45. return "[$this->limit,$this->coeffR,$this->coeffN]";
46. }
47. }
Los campos privados [$limite, $coeffR, $coeffN] se utilizarán para almacenar las columnas [limites, coeffR, coeffN] de una fila en la tabla MySQL [impots].
Además, el código del servidor utilizará una excepción personalizada, la excepción ImpotsException clase:
1. class ImpotsException extends Exception {
2.
3. public function __construct($message, $code=0) {
4. parent::__construct($message, $code);
5. }
6.
7. }
- Línea 1: La clase [ImpotsException] extiende la clase [Exception] predefinida en PHP 5
- línea 3: el constructor de la clase [ImpotsException] acepta dos parámetros:
- mensaje: un mensaje de error
- $código: un código de error
11.2.2. La capa [dao] (impots_05_dao)
La capa [dao] proporciona acceso a los datos de la base de datos:
![]() |
La capa [dao] tiene la siguiente interfaz:
1. // Dao interface
2. interface IImpotsDao {
3.
4. // returns an array of Tranche entities
5. function getData();
6. }
En IImpotsDao sólo expone la interfaz getData función. Esta función coloca las distintas filas de la tabla MySQL [dbimpots.impots] en un array de Tramo objetos.
La clase de aplicación es la siguiente:
1. <?php
2.
3. // DAO layer
4. // dependencies
5. require_once "impots_05_entities.php";
6. // constants
7. define("TABLE", "impots");
8.
9. // -----------------------------------------------------------------
10. // abstract implementation
11. abstract class ImpotsDaoWithPdo implements IImpotsDao {
12.
13. // private fields
14. private $dsn;
15. private $user;
16. private $passwd;
17. private $brackets;
18.
19. // getters and setters
20. public function getDsn() {
21. return $this->dsn;
22. }
23.
24. public function setDsn($dsn) {
25. $this->dsn = $dsn;
26. }
27.
28. public function getUser() {
29. return $this->user;
30. }
31.
32. public function setUser($user) {
33. $this->user = $user;
34. }
35.
36. public function getPasswd() {
37. return $this->passwd;
38. }
39.
40. public function setPasswd($passwd) {
41. $this->passwd = $passwd;
42. }
43.
44. // constructor
45. public function __construct($dsn, $user, $passwd) {
46. // save the parameters
47. $this->setDsn($dsn);
48. $this->setUser($user);
49. $this->setPasswd($passwd);
50. // retrieve data from the DBMS
51. // Connect ($user, $pwd) to the database $dsn
52. try {
53. // connection
54. $connection = new PDO($dsn, $user, $passwd, array(PDO::ATTR_PERSISTENT => true));
55. // read from the $TABLE table
56. $query = "select limits,coeffR,coeffN from " . TABLE;
57. // execute the $query on the $connection
58. $statement = $connection->prepare($query);
59. $statement->execute();
60. // process the query result
61. while ($columns = $statement->fetch()) {
62. $this->slices[] = new Slice($columns[0], $columns[1], $columns[2]);
63. }
64. // disconnect
65. $connection = NULL;
66. } catch (PDOException $e) {
67. // return with error
68. throw new ImpotsException($e->getMessage(), 1);
69. }
70. }
71.
72. public function getData(){
73. return $this->brackets;
74. }
75. }
- línea 5: la implementación de la interfaz [IImpotsDao] requiere las clases definidas en el script [impots_05_entities].
- línea 11: definición de una clase abstracta. Una clase abstracta es una clase que no puede ser instanciada. Una clase abstracta debe ser derivada para poder ser instanciada. Una clase puede declararse abstracta porque no puede ser instanciada (algunos de sus métodos no están definidos) o porque no queremos desea para instanciarlo. En este caso, no queremos instanciar el [ImpotsDaoWithPdo] clase. Instanciaremos clases derivadas.
- Línea 11: La clase [ImpotsDaoWithPdo] implementa la interfaz [IImpotsDao]. Por lo tanto, debe definir el parámetro getData método. Este método se encuentra en las líneas 72-74.
- Línea 14: $dsn (Nombre de la fuente de datos) es una cadena que identifica unívocamente el DBMS y la base de datos que se está utilizando.
- Línea 15: $usuario identifica al usuario que se conecta a la base de datos
- línea 16: $passwd es la contraseña del usuario anterior
- línea 17: muescas es la matriz de Tramo objetos en la que se almacenará la tabla MySQL [dbimpots.impots].
- Líneas 45-70: El constructor de la clase. Este código ya se encontraba en la versión 4, Sección 8.2. Tenga en cuenta que la construcción del objeto [ImpotsDaoWithPdo] puede fallar. En ese caso se lanza un [ImpotsException].
- Líneas 72-74: El método [getData] de la interfaz [IImpotsDao].
En [ImpotsDaoWithPdo] clase es adecuado para cualquier DBMS. El constructor de la clase, línea 45, requiere conocimientos de el de la base de datos Nombre de la fuente de datos. Esta cadena depende del DBMS utilizado. Hemos optado por no exigir al usuario del clase para saber esto Nombre de la fuente de datos. Para cada DBMS, habrá una clase específica derivada de [ImpotsDaoWithPdo]. Para el MySQL DBMS, será la siguiente clase:
1. class TaxDaoWithMySQL extends TaxDaoWithPDO {
2.
3. public function __construct($host, $port, $db, $user, $passwd) {
4. parent::__construct("mysql:host=$host;dbname=$base;port=$port", $user, $passwd);
5. }
6.
7. }
- En la línea 3, el constructor no solicita el archivo Nombre de la fuente de datos, sino simplemente el nombre de la máquina anfitriona DBMS ($host), su puerto de escucha ($port) y el nombre de la base de datos ($base).
- Línea 4: El Nombre de la fuente de datos para la base de datos MySQL se construye y se utiliza para llamar al constructor de la clase padre.
Tenga en cuenta que para adaptarse a otro DBMS, basta con escribir la clase apropiada derivada de [ImpotsDaoWithPdo]. En cada caso, la clase Nombre de la fuente de datos específico del DBMS que se está utilizando debe construirse.
11.2.3. La capa [empresarial] (impots_05_metier)
La capa [business] contiene la lógica de cálculo de los impuestos:
![]() |
La capa [business] tiene la siguiente interfaz:
1. <?php
2.
3. // business interface
4. interface IImpotsMetier {
5. public function calculateTax($married, $children, $salary);
6. }
La interfaz [IImpotsMetier] sólo expone un método, el método [calculateTax], que calcula el impuesto de un contribuyente basándose en los siguientes parámetros:
- $casado: una cadena de "sí" o "no" en función de si el contribuyente está casado o no
- niños: el número de hijos del contribuyente
- salario: el salario del contribuyente
La capa [web] proporcionará estos parámetros.
La implementación de la interfaz [IImpotsMetier] es la siguiente:
1. // dependencies
2. require_once "impots_05_dao.php";
3.
4. // ------------------------------------------------------------------
5. // implementation class
6. class BusinessTaxes implements IImpotsMetier {
7.
8. // DAO layer
9. private $dao;
10. // array of [Tranche] objects
11. private $data;
12.
13. // getter and setter
14. public function getDao() {
15. return $this->dao;
16. }
17.
18. public function setDao($dao) {
19. $this->dao = $dao;
20. }
21.
22. public function setData($data){
23. $this->data = $data;
24. }
25.
26. public function __construct($dao) {
27. // retrieve the data needed to calculate the tax
28. $this->setDao($dao);
29. $this->setData($this->dao->getData());
30. }
31.
32. public function calculateTax($married, $children, $salary) {
33. // $marié: yes, no
34. // $children: number of children
35. // $salary: annual salary
36. // number of shares
37. $married = strtolower($married);
38. if ($married == "yes")
39. $nbShares = $children / 2 + 2;
40. else
41. $nbParts = $children / 2 + 1;
42. // add 1/2 portion if there are at least 3 children
43. if ($children >= 3)
44. $nbParts+=0.5;
45. // taxable income
46. $taxableIncome = 0.72 * $salary;
47. // family quotient
48. $quotient = $taxableIncome / $numberOfChildren;
49. // is placed at the end of the limits array to stop the following loop
50. $N = count($this->data);
51. $this->data[$N - 1]->setLimit($quotient);
52. // tax calculation
53. $i = 0;
54. while ($i < $N and $quotient > $this->data[$i]->getLimit()) {
55. $i++;
56. }
57. // Since we placed $quotient at the end of the $limites array, the previous loop
58. // cannot overflow the $limites array
59. // now we can calculate the tax
60. return floor($taxableIncome * $this->data[$i]->getCoeffR() - $numberOfShares * $this->data[$i]->getCoeffN());
61. }
62.
63. }
- Línea 2: La capa [business] requiere clases de la capa [DAO] y entidades (Tramo, ImpotsException).
- línea 6: la clase [ImpotsMetier] implementa la interfaz [IimpotsMetier].
- líneas 9-11: los campos privados de la clase:
- $dao: referencia a la capa [dao]
- $datos: array de objetos de tipo [Slice] proporcionados por la capa [dao]
- Líneas 26-30: El constructor de la clase inicializa los dos campos anteriores. Recibe como parámetro una referencia a la capa [dao].
- líneas 32-61: implementación del método [calculerImpot] de la interfaz [IimpotsMetier]. Este método se introdujo en la versión 1 (sección 4.2).
11.2.4. La capa [web] (impots_05_web)
La capa [business] contiene la lógica de cálculo de los impuestos:
![]() |
La capa [web] consiste en el servicio web que responde a los clientes web. Recordemos que estos clientes realizan una petición al servicio web enviando el siguiente parámetro: params=casado,hijos,salario. Se trata de un servicio web similar a los que hemos construido en las secciones anteriores. Su código es el siguiente:
1. <?php
2.
3. // business logic layer
4. require_once "impots_05_business.php";
5.
6. // error handling
7. ini_set("display_errors", "off");
8.
9. // UTF-8 header
10. header("Content-Type: text/plain; charset=utf-8");
11.
12. // ------------------------------------------------------------------------------
13. // the tax web service
14. // definition of constants
15. $HOST = "localhost";
16. $PORT = 3306;
17. $DATABASE = "dbimpots";
18. $USER = "root";
19. $PWD = "";
20. // The data needed to calculate the tax has been placed in the MySQL table IMPOTS
21. // belonging to the $BASE database. The table has the following structure
22. // limits decimal(10,2), coeffR decimal(6,2), coeffN decimal(10,2)
23. // the parameters for taxable individuals (marital status, number of children, annual salary)
24. // are sent by the client in the form params=marital status, number of children, annual salary
25. // the results (marital status, number of children, annual salary, tax due) are returned to the client
26. // in the form <tax>tax</tax>
27. // or in the form <error>error</error>, if the parameters are invalid
28.
29. // retrieve the [business] layer from the session
30. session_start();
31. if (!isset($_SESSION['metier'])) {
32. // Instantiate the [DAO] layer and the [business] layer
33. try {
34. $_SESSION['metier'] = new ImpotsMetier(new ImpotsDaoWithMySQL($HOST, $PORT, $BASE, $USER, $PWD));
35. } catch (ImpotsException $ie) {
36. print "<error>Error: " . utf8_encode($ie->getMessage() . "</error>");
37. exit;
38. }
39. }
40. $job = $_SESSION['job'];
41.
42. // retrieve the line sent by the client
43. $params = utf8_encode(htmlspecialchars(strtolower(trim($_POST['params']))));
44. $items = explode(",", $params);
45. // there must be exactly 3 parameters
46. if (count($items) != 3) {
47. print "<error>[$params]: invalid number of parameters</error>\n";
48. exit;
49. }//if
50. // the first parameter (marital status) must be yes/no
51. $married = trim($items[0]);
52. if ($married != "yes" and $married != "no") {
53. print "<error>[$params]: Invalid first parameter</error>\n";
54. exit;
55. }//if
56. // the second parameter (number of children) must be an integer
57. if (!preg_match("/^\s*(\d+)\s*$/", $items[1], $fields)) {
58. print "<error>[$params]: Invalid second parameter</error>\n";
59. exit;
60. }//if
61. $children = $fields[1];
62. // the third parameter (salary) must be an integer
63. if (!preg_match("/^\s*(\d+)\s*$/", $items[2], $fields)) {
64. print "<error>[$params]: Invalid third parameter</error>\n";
65. exit;
66. }//if
67. $salary = $fields[1];
68. // calculate the tax
69. $tax = $occupation->calculateTax($married, $children, $salary);
70. // return the result
71. print "<tax>$tax</tax>\n";
72. // end
73. exit;
- Línea 4: La capa [web] necesita las clases de la capa [business]
- líneas 30-40: la referencia a la capa [business] tiene ámbito de sesión. Si recordamos que esta capa [business] tiene una referencia a la capa [DAO] y que esta última almacena los datos DBMS, podemos ver que:
- que la primera petición del cliente desencadenará un acceso al DBMS
- que las solicitudes posteriores del mismo cliente utilizarán los datos almacenados por la capa [DAO]. Por lo tanto, no hay acceso al DBMS.
- Línea 34: Construcción de una capa [business] trabajando con una capa [DAO] implementada para el MySQL DBMS
- Líneas 35-37: Tratamiento de un posible error en la operación anterior. En este caso, un <error>mensaje</error> se envía al cliente.
- Línea 43: Recupera el parámetro 'params' enviado por el cliente.
- Líneas 46-49: Comprueba el número de elementos encontrados en 'params'
- Líneas 51-55: Comprobación de la validez del primer dato
- Líneas 56-60: Lo mismo para el segundo dato
- líneas 62-66: lo mismo para la tercera
- Línea 69: La capa [empresa] calcula el impuesto.
- línea 71: enviar el resultado al cliente
Resultados
Recordemos que el cliente [client_impots_web_05] utiliza el siguiente archivo [data.txt]:
A partir de estas líneas (casado, hijos, salario), el cliente consulta al servidor de cálculo de impuestos y escribe los resultados en el fichero de texto [results.txt]. Tras la ejecución del cliente, el contenido de este fichero es el siguiente:
yes:2:200000:22504
no:2:200000:33388
yes:3:200000:16400
no:3:200000:22504
yes:5:50000:0
no:0:3000000:1354938
donde cada línea está en la forma (casado, hijos, salario, impuesto calculado).






