Skip to content

4. Ejemplos

4.1. Aplicación de impuestos: Introducción

Aquí presentamos la aplicación IMPOTS, que se utilizará en numerosas ocasiones a partir de ahora. Se trata de una aplicación que permite calcular el impuesto de un contribuyente. Consideramos el caso simplificado de un contribuyente que solo tiene que declarar su salario:

  • se calcula el número de partes del empleado nbParts = nbEnfants/2 + 1 si no está casado, nbEnfants/2 + 2 si está casado, donde nbEnfants es el número de hijos que tiene.
  • si tiene al menos tres hijos, cuenta con media parte adicional
  • se calcula su ingreso imponible R = 0,72 * S, donde S es su salario anual
  • se calcula su coeficiente familiar QF = R / nbParts
  • Se calcula su impuesto I. Consideremos la siguiente tabla:
12 620,0
0
0
13 190
0,05
631
15640
0,1
1290,5
24 740
0,15
2072,5
31 810
0,2
3309,5
39 970
0,25
4900
48 360
0,3
6898,5
55 790
0,35
9316,5
92 970
0,4
12106
127 860
0,45
16 754,5
151 250
0,50
23 147,5
172 040
0,55
30 710
195 000
0,60
39312
0
0,65
49 062

Cada línea tiene 3 campos. Para calcular el impuesto I, se busca la primera línea en la que QF sea menor o igual que el campo 1. Por ejemplo, si QF = 23000, se encontrará la línea

    24740        0.15        2072.5

El impuesto I es entonces igual a 0,15*R - 2072,5*nbParts. Si QF es tal que la relación QF <= campo1 nunca se cumple, entonces se utilizan los coeficientes de la última línea. En este caso:

    0                0.65        49062

lo que da como resultado el impuesto I = 0,65 * R - 49 062 * nbParts.

Los datos que definen los distintos tramos impositivos se encuentran en una base de datos ODBC-MySQL. MySQL es una versión de SGBD de dominio público que se puede utilizar en diferentes plataformas, entre ellas Windows y Linux. Con este SGBD, se ha creado una base de datos llamada dbimpots que contiene una única tabla denominada impots. El acceso a la base de datos se controla mediante un nombre de usuario y una contraseña, en este caso admimpots/mdpimpots. La siguiente captura de pantalla muestra cómo utilizar la base de datos dbimpots con MySQL:


C:\Program Files\EasyPHP\mysql\bin>mysql -u admimpots -p
Enter password: *********
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 18 to server version: 3.23.49-max-nt

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> use dbimpots;
Database changed

mysql> show tables;
+--------------------+
| Tables_in_dbimpots |
+--------------------+
| impots             |
+--------------------+
1 row in set (0.00 sec)

mysql> describe impots;
+---------+--------+------+-----+---------+-------+
| Field   | Type   | Null | Key | Default | Extra |
+---------+--------+------+-----+---------+-------+
| limites | double | YES  |     | NULL    |       |
| coeffR  | double | YES  |     | NULL    |       |
| coeffN  | double | YES  |     | NULL    |       |
+---------+--------+------+-----+---------+-------+
3 rows in set (0.02 sec)

mysql> select * from impots;
+---------+--------+---------+
| limites | coeffR | coeffN  |
+---------+--------+---------+
|   12620 |      0 |       0 |
|   13190 |   0.05 |     631 |
|   15640 |    0.1 |  1290.5 |
|   24740 |   0.15 |  2072.5 |
|   31810 |    0.2 |  3309.5 |
|   39970 |   0.25 |    4900 |
|   48360 |    0.3 |    6898 |
|   55790 |   0.35 |  9316.5 |
|   92970 |    0.4 |   12106 |
|  127860 |   0.45 |   16754 |
|  151250 |    0.5 | 23147.5 |
|  172040 |   0.55 |   30710 |
|  195000 |    0.6 |   39312 |
|       0 |   0.65 |   49062 |
+---------+--------+---------+
14 rows in set (0.00 sec)

mysql>quit

La base de datos dbimpots se transforma en la fuente de datos ODBC de la siguiente manera:

  • se inicia el administrador de fuentes de datos ODBC de 32 bits

Image

  • se utiliza el botón [Add] para agregar una nueva fuente de datos ODBC

Image

  • Se selecciona el controlador MySQL y se ejecuta [Terminer]
  • El controlador MySQL solicita cierta información:
1
el nombre DSN que se le dará a la fuente de datos ODBC —puede ser cualquiera—
2
la máquina en la que se ejecuta el SGBD MySQL —en este caso, localhost—. Es interesante señalar que la base de datos podría ser una base de datos remota. Las aplicaciones locales que utilizan la fuente de datos ODBC no se darían cuenta de ello. Este sería el caso, en particular, de nuestra aplicación PHP.
3
la base de datos MySQL que se utilizará. MySQL es un SGBD que administra bases de datos relacionales, las cuales son conjuntos de tablas conectadas entre sí mediante relaciones. Aquí se indica el nombre de la base de datos administrada.
4
el nombre de un usuario con derecho de acceso a esta base de datos
5
su contraseña

4.2. Aplicación de impuestos: la clase ImpotsDSN

Nuestra aplicación se basará en la clase PHP ImpotsDSN, que tendrá los siguientes atributos, constructor y método:

<?php
...
     // atributos
    var $limites;        // tabla de límites
    var $coeffR;        // tabla de coeffR
    var $coeffN;        // tabla de coeffN
    var $erreurs;        // tabla de errores
     // fabricante
    function ImpotsDSN($impots){...}
     // método
    function calculer($personne){...}
  • En la exposición del problema vimos que necesitábamos tres series de datos
12620.0
0
0
13190
0,05
631
15640
0,1
1290,5
24 740
0,15
2072,5
31 810
0,2
3309,5
39 970
0,25
4900
48 360
0,3
6898,5
55 790
0,35
9316,5
92 970
0,4
12106
127 860
0,45
16 754,5
151 250
0,50
23 147,5
172 040
0,55
30 710
195 000
0,60
39312
0
0,65
49062

La primera columna se colocará en el atributo $limites de la clase ImotsDSN, la segunda en el atributo $coeffR y la tercera en $coeffN. Estos tres atributos son inicializados por el constructor de la clase. Este recupera los datos de una fuente de datos ODBC. Pueden producirse diversos errores durante la construcción del objeto. Estos errores se registran en el atributo $erreurs en forma de una tabla de mensajes de error.

  • El constructor recibe como parámetro un diccionario $impots con los siguientes campos:
// dsn: nombre DSN de la fuente de datos ODBC que contiene los valores de las tablas de límites, coeffR, coeffN
// user: nombre de un usuario con permiso de lectura sobre la fuente ODBC
// pwd: su contraseña
// tabla: nombre de la tabla que contiene los valores (límites, coeffR, coeffN)
// límites: nombre de la columna que contiene los valores límite
// coeffR: nombre de la columna que contiene los valores coeffR       
// coeffN: nombre de la columna que contiene los valores coeffN

El parámetro del constructor proporciona toda la información necesaria para leer los datos de la fuente de datos ODBC

  • Una vez creado el objeto ImpotsDSN, los usuarios de la clase podrán llamar al método calculerImpots del objeto. Este método recibe como parámetro un diccionario $personne:
       // $persona["marié"]: sí, no
       // $persona["enfants"]: número de hijos
       // $persona["salaire"]: salario anual

La clase completa es la siguiente:

<?php
   // definición de una clase objImpots
  class ImpotsDSN{

     // atributos: las 3 tablas de datos
    var $limites;        // tabla de límites
    var $coeffR;        // tabla de coeffR
    var $coeffN;        // tabla de coeffN
    var $erreurs;        // tabla de errores

     // constructor
    function ImpotsDSN($impots){
       // $impots: diccionario que contiene los siguientes campos
       // dsn: nombre DSN de la fuente de datos ODBC que contiene los valores de las tablas de límites, coeffR, coeffN
       // user: nombre de un usuario con derecho de lectura sobre la fuente ODBC
       // pwd: su contraseña
       // tabla: nombre de la tabla que contiene los valores (límites, coeffR, coeffN)
       // límites: nombre de la columna que contiene los valores límite
       // coeffR: nombre de la columna que contiene los valores coeffR       
       // coeffN: nombre de la columna que contiene los valores coeffN

       // al principio no hay errores
      $this->erreurs=array();

             // verificación de la llamada
      if (! isset($impots[dsn]) || ! isset($impots[user]) ||! isset($impots[pwd]) ||! isset($impots[table]) ||
          ! isset($impots[limites]) ||! isset($impots[coeffR]) ||! isset($impots[coeffN])){
         // error
        $this->erreurs[]="Appel incorrect";
         // fin
        return;
      }//si

       // apertura de la base de datos DSN
      $connexion=odbc_connect($impots[dsn],$impots[user],$impots[pwd]);
       // ¿error?
      if(! $connexion){
         // error
        $this->erreurs[]="Impossible d'ouvrir la base DSN [$impots[dsn]] (".odbc_error().")";
         // fin
        return;
      }//si

             // envío de una consulta a la base de datos
      $requête=odbc_prepare($connexion,"select $impots[limites],$impots[coeffR],$impots[coeffN] from $impots[table]");
            if(! odbc_execute($requête)){
         // error
        $this->erreurs[]="Impossible d'expoiter la base DSN [$impots[dsn]] (".odbc_error().")";
         // fin
        odbc_close($connexion);
        return;
      }//si

       // procesamiento de los resultados de la consulta
      $this->limites=array();
      $this->coeffR=array();
      $this->coeffN=array();
      while(odbc_fetch_row($requête)){     
          // una fila más
        $this->limites[]=odbc_result($requête,$impots[limites]);
        $this->coeffR[]=odbc_result($requête,$impots[coeffR]);
        $this->coeffN[]=odbc_result($requête,$impots[coeffN]);
      }//mientras

       // cierre de la base
      odbc_close($connexion);
    }//constructor                

    // --------------------------------------------------------------------------
    function calculer($personne){
       // $persona["marié"]: sí, no
       // $persona["enfants"]: número de hijos
       // $persona["salaire"]: salario anual

       // ¿El objeto se encuentra en buen estado?
      if (! is_array($this->erreurs) || count($this->erreurs)!=0) return -1;

            // verificación de la llamada
      if (! isset($personne[marié]) || ! isset($personne[enfants]) ||! isset($personne[salaire])){
         // error
        $this->erreurs[]="Appel incorrect";
         // fin
        return -1;
      }//si

       // ¿Son correctos los parámetros?
      $personne[marié]=strtolower($personne[marié]);
      if($personne[marié]!=oui && $personne[marié]!=non){
           // error
        $this->erreurs[]="Statut marital [$personne[marié]] incorrect";
      }//si
      if(! preg_match("/^\s*\d{1,3}\s*$/",$personne[enfants])){
          // error
        $this->erreurs[]="Nombre d'enfants [$personne[enfants]] incorrect";
      }//si
      if(! preg_match("/^\s*\d+\s*$/",$personne[salaire])){
          // error
        $this->erreurs[]="salaire [$personne[salaire]] incorrect";
      }//si
       // ¿Hay errores?
      if(count($this->erreurs)!=0) return -1;

      // número de porciones
      if($personne[marié]==oui) $nbParts=$personne[enfants]/2+2;
        else $nbParts=$personne[enfants]/2+1;
       // media parte más si hay al menos tres hijos
      if($personne[enfants]>=3) $nbParts+=0.5;
       // ingresos imponibles
      $revenuImposable=0.72*$personne[salaire];
       // coeficiente familiar
      $quotient=$revenuImposable/$nbParts;
       // se coloca al final de la tabla de límites para detener el bucle siguiente
      $this->limites[$this->nbLimites]=$quotient;
       // cálculo del impuesto
      $i=0;
      while($quotient>$this->limites[$i]) $i++;
      // debido a que se colocó $quotient al final de la tabla $limites, el bucle anterior
       // no puede salirse de los límites de la tabla $limites
       // ahora podemos calcular el impuesto
      return floor($revenuImposable*$this->coeffR[$i]-$nbParts*$this->coeffN[$i]);
    }//calculImpots
  }//Clase de impuestos
?>

Un programa de prueba podría ser el siguiente:

<?php
     // bibliotecas
  include "ImpotsDSN.php";

     // creación de un objeto de impuestos
  $conf=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

  $objImpots=new ImpotsDSN($conf);

  // ¿Hay errores?
  if(count($objImpots->erreurs)!=0){
       // mensaje
    echo "Les erreurs suivantes se sont produites :\n";
    $erreurs=$objImpots->erreurs;
    for($i=0;$i<count($erreurs);$i++){
        echo "$erreurs[$i]\n";
    }//para
    // fin
    exit(1);
  }//si

   // pruebas
  calculerImpots($objImpots,oui,2,200000);
  calculerImpots($objImpots,non,2,200000);
  calculerImpots($objImpots,oui,3,200000);
  calculerImpots($objImpots,non,3,200000);
  calculerImpots($objImpots,array(),array(),array());

  // fin
  exit(0);

  // -----------------------------------------------------------
  function calculerImpots($objImpots,$marié,$enfants,$salaire){
       // eco
        echo "impots($marié,$enfants,$salaire)\n";    

       // cálculo del impuesto
    $personne=array(marié=>$marié,enfants=>$enfants,salaire=>$salaire);        
      $montant=$objImpots->calculer($personne);
     // ¿Hay errores?
    if(count($objImpots->erreurs)!=0){
         // mensaje
      echo "Les erreurs suivantes se sont produites :\n";
      $erreurs=$objImpots->erreurs;
      for($i=0;$i<count($erreurs);$i++){
          echo "$erreurs[$i]\n";
      }//para
    }else echo "montant=$montant\n";
  }//calculerImp
  • El programa de prueba se encarga de «incluir» el archivo que contiene la clase ImpotsDSN
  • y luego crea un objeto objImpots de la clase ImpotsDSN, pasando al constructor del objeto la información que necesita
  • Una vez creado este objeto, se llamará cinco veces al método calculer de dicho objeto

Los resultados obtenidos son los siguientes:


dos>e:\php43\php.exe test.php
impots(oui,2,200000)
montant=22504
impots(non,2,200000)
montant=33388
impots(oui,3,200000)
montant=16400
impots(non,3,200000)
montant=22504
impots(Array,Array,Array)
Les erreurs suivantes se sont produites :
Statut marital [array] incorrect
Nombre d'enfants [Array] incorrect
salaire [Array] incorrect

A partir de ahora utilizaremos la clase ImpotsDSN sin volver a dar su definición.

4.3. Aplicación de impuestos: versión 1

Ahora presentamos la versión 1 de la aplicación IMPOTS. Nos situamos en el contexto de una aplicación web que presentaría una interfaz HTML a un usuario con el fin de obtener los tres parámetros necesarios para el cálculo del impuesto:

  • el estado civil (casado o soltero)
  • el número de hijos
  • el salario anual

Image

La visualización del formulario se realiza mediante la siguiente página PHP:

<?php //formulario de impuestos ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
           // borrado del formulario
        with(document.frmImpots){
            optMarie[0].checked=false;
          optMarie[1].checked=true;
          txtEnfants.value="";
          txtSalaire.value="";
          txtImpots.value="";
        }//con
      }//borrar      
        </script>
  </head>

  <body background="/poly/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                <input type="radio" name="optMarie" value="oui" <?php echo $requête->chkoui ?>>oui
              <input type="radio" name="optMarie" value="non" <?php echo $requête->chknon ?>>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="5" name="txtEnfants" value="<?php echo $requête->enfants ?>"></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value="<?php echo $requête->salaire ?>"></td>
          </tr>
          <tr>
              <td><font color="green">Impôt</font></td>
            <td><input type="text" size="10" name="txtImpots" value="<?php echo $requête->impots ?>" readonly></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="submit" value="Calculer"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
    <?php
         // ¿Hay errores?
      if(count($requête->erreurs)!=0){
           // mostrar errores
        echo "<hr>\n<font color=\"red\">\n";
        echo "Les erreurs suivantes se sont produites<br>";
        echo "<ul>";
        for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i]."</li>\n";
        }
        echo "</ul>\n</font>\n";
      }//si 
   ?>
 </body>
</html>

La página PHP se limita a mostrar la información que le envía el programa principal de la aplicación en la variable $requête, que es un objeto con los siguientes campos:

chkoui, chknon
atributos de los botones de opción «sí» y «no»: pueden tener los valores "checked" o "" para activar o desactivar el botón de opción correspondiente
enfants
el número de hijos del contribuyente
salaire
su salario anual
impots
el monto del impuesto a pagar
erreurs
una posible lista de errores; puede estar vacía.

La página enviada al cliente contiene un script de JavaScript con una función effacer asociada al botón «Effacer», cuya función es restablecer el formulario a su estado inicial: botón sin marcar, campos de entrada vacíos. Cabe señalar que este resultado no se podía obtener con un botón HTML de tipo «reset». De hecho, cuando se utiliza este tipo de botón, el navegador restablece el formulario al estado en el que lo recibió. Sin embargo, en nuestra aplicación, el navegador recibe formularios que pueden no estar vacíos.

La aplicación que procesa el formulario anterior es la siguiente:

<?php
     // procesa el formulario de impuestos

     // bibliotecas
  include "ImpotsDSN.php";

  // Configuración de la aplicación
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  $formulaireImpots="impots_form.php";
  $erreursImpots="impots_erreurs.php";
  $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

   // se recuperan los parámetros
  $requête->marié=$_POST["optMarie"];
  $requête->enfants=$_POST["txtEnfants"];
  $requête->salaire=$_POST["txtSalaire"];

   // ¿Tenemos todos los parámetros?
  if(! isset($requête->marié) || ! isset($requête->enfants) || ! isset($requête->salaire)){
       // preparación del formulario en blanco
    $requête->chkoui="";
    $requête->chknon="checked";
    $requête->enfants="";
    $requête->salaire="";
    $requête->impots="";
    $requête->erreurs=array();
     // visualización del formulario
    include $formulaireImpots;
     // fin
    exit(0);
  }//si

   // verificación de parámetros
  $requête=vérifier($requête);

   // ¿Hay errores?
  if(count($requête->erreurs)!=0){
       // visualización del formulario
    include "$formulaireImpots";
     // fin
    exit(0);
  }//si

   // cálculo de la respuesta
  $requête=calculerImpots($bdImpots,$requête);

   // ¿Hay errores?
  if(count($requête->erreurs)!=0){
       // visualización del formulario de errores
    include "$erreursImpots";
     // fin
    exit(0);
  }//si

   // visualización del formulario
  include "$formulaireImpots";
   // fin
  exit(0);

  // --------------------------------------------------------
  function vérifier($requête){
       // verifica la validez de los parámetros de la solicitud

     // al inicio no hay errores
    $requête->erreurs=array();

     // ¿Estado válido?
    $requête->marié=strtolower($requête->marié);
    if($requête->marié!="oui" && $requête->marié!="non"){
         // un error
      $requête->erreurs[]="Statut marital [$requête->marié] incorrect";
    }

     // ¿Número de hijos válido?
    if(! preg_match("/^\s*\d+\s*$/",$requête->enfants)){
        // un error
      $requête->erreurs[]="Nombre d'enfants [$requête->enfants] incorrect";
    }

     // ¿Salario válido?
    if(! preg_match("/^\s*\d+\s*$/",$requête->salaire)){
        // un error
      $requête->erreurs[]="Salaire [$requête->salaire] incorrect";
    }

     // estado de los botones de opción
    if($requête->marié=="oui"){
        $requête->chkoui="checked";
      $requête->chknon="";
    }else{
        $requête->chknon="checked";
      $requête->chkoui="";
    }

     // se envía la consulta
    return $requête;
  }//verificar

  // --------------------------------------------------------
  function calculerImpots($bdImpots,$requête){
       // calcula el monto del impuesto

     // $bdImpots: diccionario que contiene la información necesaria para leer la fuente de datos ODBC
     // $requête: la consulta que contiene la información sobre el contribuyente

     // se crea un objeto ImpotsDSN 
    $objImpots=new ImpotsDSN($bdImpots);

    // ¿Hay errores?
    if(count($objImpots->erreurs)!=0){
         // se incluyen los errores en la consulta
      $requête->erreurs=$objImpots->erreurs;
      // listo
      return $requête;
    }//si

    // cálculo del impuesto
    $personne=array(marié=>"$requête->marié",enfants=>"$requête->enfants",salaire=>"$requête->salaire");
        $requête->impots=$objImpots->calculer($personne);

     // se devuelve el resultado
    return $requête;
  }//calculerImpots

Comentarios:

  • En primer lugar, se incluye la clase ImpotsDSN. Esta es la base para el cálculo de impuestos y nos ocultará el acceso a la base de datos
  • Se realizan varias configuraciones iniciales con el objetivo de facilitar el mantenimiento de la aplicación. Si cambian los parámetros, se modificarán sus valores en esta sección. Por lo general, estos valores de configuración se almacenan en un archivo o en una base de datos.
  • Se recuperan los tres parámetros del formulario correspondientes a los campos HTML, optMarie, txtEnfants y txtSalaire.
  • Estos parámetros pueden estar ausentes total o parcialmente. Este será el caso, en particular, durante la solicitud inicial del formulario. En ese caso, simplemente se envía un formulario vacío.
  • A continuación, se verifica la validez de los tres parámetros recuperados. Si resultan incorrectos, el formulario se devuelve tal como se ingresó, pero además se incluye una lista de errores.
  • Una vez verificada la validez de los tres parámetros, se puede realizar el cálculo del impuesto. Este se calcula mediante la función calculerImpots. Esta función crea un objeto de tipo ImpotsDSN y utiliza el método calculer de dicho objeto.
  • La creación del objeto ImpotsDSN puede fallar si la base de datos no está disponible. En ese caso, la aplicación muestra una página de error específica que indica los errores que se han producido.
  • Si todo salió bien, el formulario se devuelve tal como se ingresó, pero además incluye el monto del impuesto a pagar.

La página de errores es la siguiente:

<?php    // página de error ?>
<html>
    <head>
      <title>Application impôts indisponible</title>
  </head>
  <body background="/poly/impots/images/standard.jpg">
      <h3>Calcul d'impôts</h3>
    <hr>
    Application indisponible. Recommencez ultérieurement.<br><br>
    <font color="red">
           Les erreurs suivantes se sont produites<br>
        <ul>
      <?php
           // visualización de errores
        for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i]."</li>\n";
        }
      ?>
        </ul>
    </font>
  </body>
</html>

A continuación se muestran algunos ejemplos de errores. En primer lugar, se ingresa una contraseña incorrecta en la base de datos:

Image

Se ingresan datos incorrectos en el formulario:

Image

Por último, se ingresan valores correctos:

Image

4.4. Aplicación de impuestos: versión 2

En el ejemplo anterior, el servidor verifica la validez de los parámetros txtEnfants y txtSalaire del formulario. Aquí proponemos verificarla mediante un script de JavaScript incluido en la página del formulario. De esta manera, es el navegador el que verifica los parámetros. El servidor solo se consulta si estos son válidos. Así se ahorra «ancho de banda». La página de visualización impots-form.php queda así:

<?php //formulario de impuestos ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
........
      }//borrar

      //------------------------------
      function calculer(){
           // verificación de los parámetros antes de enviarlos al servidor
        with(document.frmImpots){
          //número de hijos
          champs=/^\s*(\d+)\s*$/.exec(txtEnfants.value);
          if(champs==null){
            // la plantilla no se ha verificado
            alert("Le nombre d'enfants n'a pas été donné ou est incorrect");
            nbEnfants.focus();
            return;
          }//si
           //salario
          champs=/^\s*(\d+)\s*$/.exec(txtSalaire.value);
          if(champs==null){
            // el modelo no se ha verificado
            alert("Le salaire n'a pas été donné ou est incorrect");
            salaire.focus();
            return;
          }//si
          // Todo está bien: se envía el formulario al servidor
          submit();
        }//con
      }//calcular            
        </script>
  </head>

  <body background="/poly/impots/images/standard.jpg">
............
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
........
 </body>
</html>

Cabe destacar los siguientes cambios:

  • el botón Calculer ya no es del tipo submit, sino del tipo button, asociado a una función llamada calculer. Es esta función la que analizará los campos txtEnfants y txTsalaire. Si son correctos, los valores del formulario se enviarán al servidor (submit); de lo contrario, se mostrará un mensaje de error.

A continuación se muestra un ejemplo de lo que se vería en caso de error:

Image

4.5. Aplicación de impuestos: versión 3

Modificamos ligeramente la aplicación para introducir el concepto de sesión. Ahora consideramos que la aplicación es una simulación del cálculo de impuestos. De esta manera, un usuario puede simular diferentes «configuraciones» de contribuyente y ver cuál sería el impuesto a pagar en cada caso. La página web a continuación muestra un ejemplo de lo que se podría obtener:

Image

El programa para mostrar la página anterior es el siguiente:

<?php //formulario de impuestos ?>

<html>
    <head>
      <title>impots</title>
    <script language="JavaScript" type="text/javascript">
        function effacer(){
...
      }//borrar

      //------------------------------
      function calculer(){
...
      }//calcular            
        </script>
  </head>

  <body background="/poly/impots/images/standard.jpg">
      <center>
        Calcul d'impôts
        <hr>
      <form name="frmImpots" method="POST">
          <table>
            <tr>
              <td>Etes-vous marié(e)</td>
            <td>
                <input type="radio" name="optMarie" value="oui" <?php echo $requête[chkoui] ?>>oui
              <input type="radio" name="optMarie" value="non" <?php echo $requête[chknon] ?>>non
            </td>
          </tr>
          <tr>
              <td>Nombre d'enfants</td>
            <td><input type="text" size="5" name="txtEnfants" value="<?php echo $requête[enfants] ?>"></td>
          </tr>
          <tr>
              <td>Salaire annuel</td>
            <td><input type="text" size="10" name="txtSalaire" value="<?php echo $requête[salaire] ?>"></td>
          </tr>
          <tr>
              <td><font color="green">Impôt</font></td>
            <td><input type="text" size="10" name="txtImpots" value="<?php echo $requête[impots] ?>" readonly></td>
          </tr>
          <tr></tr>
          <tr>
              <td><input type="button" value="Calculer" onclick="calculer()"></td>
            <td><input type="button" value="Effacer" onclick="effacer()"></td>
          </tr>
        </table>
      </form>
    </center>
    <?php
         // ¿Hay errores?
      if(count($requête[erreurs])!=0){
           // mostrar errores
        echo "<hr>\n<font color=\"red\">\n";
        echo "Les erreurs suivantes se sont produites<br>";
        echo "<ul>";
        for($i=0;$i<count($requête[erreurs]);$i++){
            echo "<li>".$requête[erreurs][$i]."</li>\n";
        }
        echo "</ul>\n</font>\n";
      }//si 
         // ¿Hay simulaciones?
      else if(count($requête[simulations])!=0){
          // Visualización de las simulaciones
        echo "<hr>\n<h2>Résultats des simulations</h2>\n";
        echo "<table border=\"1\">\n";
        echo "<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>\n";
        for($i=0;$i<count($requête[simulations]);$i++){
            echo "<tr>".
              "<td>".$requête[simulations][$i][0]."</td>".
              "<td>".$requête[simulations][$i][1]."</td>".
              "<td>".$requête[simulations][$i][2]."</td>".
              "<td>".$requête[simulations][$i][3]."</td>".
            "</tr>\n";                                    
        }//for
        echo "</table>\n";
      }//si 
   ?>
 </body>
</html>

El programa de visualización presenta diferencias menores con respecto a su versión anterior:

  • el diccionario recibe un diccionario $requête en lugar de un objeto $requête. Por lo tanto, se escribe $requête[enfants] y no $requête->hijos.
  • Este diccionario tiene un campo simulations que es una matriz bidimensional. $requête[simulations][i] es la simulación n.º i. Esta, a su vez, es una matriz de cuatro cadenas de caracteres: estado civil, número de hijos, salario anual e impuesto a pagar.

El programa principal, por su parte, ha evolucionado más profundamente:

<?php
     // procesa el formulario de impuestos

     // bibliotecas
  include "ImpotsDSN.php";

   // inicio de sesión
  session_start();

   // configuración de la aplicación
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  $formulaireImpots="impots_form.php";
  $erreursImpots="impots_erreurs.php";
  $bdImpots=array(dsn=>"mysql-dbimpots",user=>admimpots,pwd=>mdpimpots,
      table=>impots,limites=>limites,coeffR=>coeffR,coeffN=>coeffN);

     // se recuperan los parámetros de la sesión
  $session=$_SESSION["session"];
   // ¿Sesión válida?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // se inicia una nueva sesión
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // ¿Hay errores?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // se muestra la página de errores
        include $erreursImpots;
       // fin
      $session=array();
      terminerSession($session);
    }//si
  }//si

  // se recuperan los parámetros del intercambio en curso
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // ¿Se tienen todos los parámetros?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // se muestra el formulario en blanco
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // fin
    terminerSession($session);
  }//si

   // verificación de parámetros
  $requête=vérifier($requête);

   // ¿Hay errores?
  if(count($requête[erreurs])!=0){
       // visualización del formulario
    include "$formulaireImpots";
     // fin
    terminerSession($session);
  }//si

   // cálculo del impuesto a pagar
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));

   // una simulación más
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // visualización del formulario
  include "$formulaireImpots";

   // fin
  terminerSession($session);

  // --------------------------------------------------------
  function vérifier($requête){
       // verifica la validez de los parámetros de la consulta

     // al inicio no hay errores
    $requête[erreurs]=array();

     // ¿Estado válido?
    $requête[marié]=strtolower($requête[marié]);
    if($requête[marié]!="oui" && $requête[marié]!="non"){
         // un error
      $requête[erreurs][]="Statut marital [$requête[marié]] incorrect";
    }

     // ¿Número de hijos válido?
    if(! preg_match("/^\s*\d+\s*$/",$requête[enfants])){
        // un error
      $requête[erreurs][]="Nombre d'enfants [$requête[enfants]] incorrect";
    }

     // ¿Salario válido?
    if(! preg_match("/^\s*\d+\s*$/",$requête[salaire])){
        // un error
      $requête[erreurs][]="Salaire [$requête[salaire]] incorrect";
    }

     // estado de los botones de opción
    if($requête[marié]=="oui"){
        $requête[chkoui]="checked";
      $requête[chknon]="";
    }else{
        $requête[chknon]="checked";
      $requête[chkoui]="";
    }

     // se envía la solicitud
    return $requête;
  }//verificar

  // --------------------------------------------------------
  function terminerSession($session){
       // se guarda la sesión
    $_SESSION[session]=$session;
     // fin del script
    exit(0);
  }//terminerSession
  • En primer lugar, esta aplicación maneja una sesión. Por lo tanto, se inicia una sesión al comienzo del script:
  session_start();
  • La sesión almacena una sola variable: el diccionario $session. Este diccionario tiene dos campos:
    • objImpots: un objeto ImpotsDSN. Este objeto se almacena en la sesión del cliente para evitar accesos sucesivos innecesarios a la base de datos ODBC.
    • simulaciones: la tabla de simulaciones para recordar, de un intercambio a otro, las simulaciones de los intercambios anteriores.
  • Una vez iniciada la sesión, se recupera la variable $session. Si esta aún no existe, significa que la sesión está comenzando. Entonces se crea un objeto ImpotsDSN y una tabla de simulaciones vacía. Si es necesario, se reportan los errores.
<?php
...
     // se recuperan los parámetros de la sesión
  $session=$_SESSION["session"];
   // ¿Sesión válida?
  if(! isset($session) || ! isset($session[objImpots]) || ! isset($session[simulations])){
       // se inicia una nueva sesión
    $session=array(objImpots=>new ImpotsDSN($bdImpots),simulations=>array());
    // ¿Hay errores?
    if(count($session[objImpots]->erreurs)!=0){
      $requête=array(erreurs=>$session[objImpots]->erreurs);
         // se muestra la página de errores
        include $erreursImpots;
       // fin
      $session=array();
      terminerSession($session);
    }//si
  }//si
  • Una vez que la sesión se ha inicializado correctamente, se recuperan los parámetros del intercambio. Si no se encuentran todos los parámetros esperados, se envía un formulario en blanco.
<?php
...
   // se recuperan los parámetros del intercambio en curso
  $requête[marié]=$_POST["optMarie"];
  $requête[enfants]=$_POST["txtEnfants"];
  $requête[salaire]=$_POST["txtSalaire"];

   // ¿Se tienen todos los parámetros?
  if(! isset($requête[marié]) || ! isset($requête[enfants]) || ! isset($requête[salaire])){
       // se muestra el formulario en blanco
    $requête=array(chkoui=>"",chknon=>"checked",enfants=>"",salaire=>"",impots=>"",
            erreurs=>array(),simulations=>array());    
    include $formulaireImpots;
     // fin
    terminerSession($session);
  }//si
  • Si todos los parámetros están correctos, se verifican. Si hay errores, se señalan:
<?php
...
   // verificación de parámetros
  $requête=vérifier($requête);

   // ¿Hay errores?
  if(count($requête[erreurs])!=0){
       // visualización del formulario
    include "$formulaireImpots";
     // fin
    terminerSession($session);
  }//si
  • Si los parámetros son correctos, se calcula el impuesto:
<?php
...
   // cálculo del impuesto a pagar
  $requête[impots]=$session[objImpots]->calculer(array(marié=>$requête[marié],
        enfants=>$requête[enfants],salaire=>$requête[salaire]));
  • La simulación en curso se agrega a la tabla de simulaciones y esta se envía al cliente junto con el formulario:
<?php
...
   // una simulación más
  $session[simulations][]=array($requête[marié],$requête[enfants],$requête[salaire],$requête[impots]);
  $requête[simulations]=$session[simulations];

   // visualización del formulario
  include "$formulaireImpots";

   // fin
  terminerSession($session);
  • En todos los casos, el script finaliza guardando la variable $session en la sesión. Esto se realiza en el procedimiento terminerSession.
<?php
...

  function terminerSession($session){
       // se guarda la sesión
    $_SESSION[session]=$session;
     // fin del script
    exit(0);
  }//terminerSession

Terminemos presentando el programa para mostrar los errores de acceso a la base de datos (impots-erreurs.php):

<?php    // página de error ?>
<html>
    <head>
      <title>Application impôts indisponible</title>
  </head>
  <body background="/poly/impots/images/standard.jpg">
      <h3>Calcul d'impôts</h3>
    <hr>
    Application indisponible. Recommencez ultérieurement.<br><br>
    <font color="red">
           Les erreurs suivantes se sont produites<br>
        <ul>
      <?php
           // visualización de errores
        for($i=0;$i<count($requête[erreurs]);$i++){
            echo "<li>".$requête[erreurs][$i]."</li>\n";
        }
      ?>
        </ul>
    </font>
  </body>
</html>

Aquí también, el objeto $requête ha sido reemplazado por un diccionario $requête.

4.6. Aplicación de impuestos: versión 4

Ahora crearemos una aplicación independiente que será un cliente web de la aplicación web impots anterior. La aplicación será una aplicación de consola que se iniciará desde una ventana DOS:


dos>e:\php43\php.exe cltImpots.php
Syntaxe cltImpots.php urlImpots marié enfants salaire [jeton]

Los parámetros del cliente web cltImpots son los siguientes:

     // sintaxis $0 urlImpots casado hijos salario token
   // cliente de un servicio de impuestos que opera en urlImpots
   // envía a este servicio los tres datos: estado civil, hijos, salario
   // posiblemente junto con el token de sesión, si se ha proporcionado
   // muestra la tabla de simulaciones de la sesión

A continuación se muestran algunos ejemplos de uso. En primer lugar, un primer ejemplo sin token de sesión.

dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php sí 2 200000

Jeton de session=[a6297317667bc981c462120987b8dd18]

Simulations :
[Marié,Enfants,Salaire annuel (F),Impôts à payer (F)]
[oui,2,200000,22504]

Hemos recuperado correctamente el monto del impuesto a pagar (22504 f). También hemos recuperado el token de sesión. Ahora podemos usarlo para una segunda consulta:

dos>e:\php43\php.exe cltImpots.php http://localhost/poly/impots/6/impots.php sí 3 200000 a6297317667bc981c462120987b8dd18

Jeton de session=[a6297317667bc981c462120987b8dd18]

Simulations :
[Marié,Enfants,Salaire annuel (F),Impôts à payer (F)]
[oui,2,200000,22504]
[oui,3,200000,16400]

Efectivamente, obtenemos la tabla de simulaciones enviada por el servidor web. El token recuperado sigue siendo, por supuesto, el mismo. Si consultamos el servicio web sin que la base de datos se haya iniciado, obtenemos el siguiente resultado:

dos>e:\php43\php.exe cltImpots2.php http://localhost/poly/impots/6/impots.php sí 3 2 000 000

Jeton de session=[8369014d5053212bc42f64bbdfb152ee]

Les erreurs suivantes se sont produites :
Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)

Al desarrollar un cliente web programado, es necesario saber exactamente qué envía el servidor en respuesta a las diferentes solicitudes posibles de un cliente. El servidor envía un conjunto de líneas HTML que incluyen información útil y otra que solo sirve para el diseño HTML. Las expresiones regulares de PHP pueden ayudarnos a encontrar la información útil en el flujo de líneas enviadas por el servidor. Para ello, necesitamos conocer el formato exacto de las diferentes respuestas del servidor. Para ello, podemos consultar el servicio web con un navegador y revisar el código fuente que se ha enviado. Cabe señalar que este método no permite conocer los encabezados de la respuesta del servidor web. A veces resulta útil conocerlos. En ese caso, podemos utilizar uno de los dos clientes web genéricos que vimos en el capítulo anterior.

Si repetimos los ejemplos anteriores, este es el código HTML recibido por el navegador para la tabla de simulaciones:

<h2>Résultats des simulations</h2>
<table border="1">
<tr><td>Marié</td><td>Enfants</td><td>Salaire annuel (F)</td><td>Impôts à payer (F)</td></tr>
<tr><td>oui</td><td>2</td><td>200000</td><td>22504</td></tr>
<tr><td>oui</td><td>3</td><td>200000</td><td>16400</td></tr>
</table>

Esta es una tabla HTML, la única que aparece en el documento enviado. Por lo tanto, la expresión regular "|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|" debe permitir localizar en el documento las diferentes simulaciones recibidas. Cuando la base de datos no está disponible, se recibe el siguiente documento:

    Application indisponible. Recommencez ultérieurement.<br><br>
    <font color="red">
           Les erreurs suivantes se sont produites<br>
        <ul>
      <li>Impossible d'ouvrir la base DSN [mysql-dbimpots] (S1000)</li>
        </ul>
    </font>

Para recuperar el error, el cliente web deberá buscar en la respuesta del servidor web las líneas que correspondan al modelo: "|<li>(.+?)</li>|".

Ahora contamos con las pautas sobre qué hacer cuando el usuario solicita el cálculo del impuesto desde el cliente de consola anterior:

  • verificar que todos los parámetros sean válidos y, si es necesario, señalar los errores.
  • conectarse al URL proporcionado como primer parámetro. Para ello, seguiremos el modelo del cliente web genérico ya presentado y analizado
  • en el flujo de la respuesta del servidor, utilizar expresiones regulares para:
    • encontrar los mensajes de error
    • encontrar los resultados de las simulaciones

El código del cliente cltImpots.php es el siguiente:

<?php
     // sintaxis $0 urlImpots casado hijos salario token
   //, cliente de un servicio de impuestos que opera en urlImpots
   // envía a este servicio tres datos: estado civil, hijos y salario
   // posiblemente junto con el token de sesión, si se ha pasado
   // muestra la tabla de simulaciones de la sesión

   // verificación del número de argumentos
  if(count($argv)!=5 && count($argv)!=6){
       // error
    fwrite(STDERR,"Syntaxe $argv[0] urlImpots marié enfants salaire [jeton]\n");
     // fin
    exit(1);
  }//si

   // se toma nota del URL
  $urlImpots=analyseURL($argv[1]);
  if(isset($urlImpots[erreur])){
       // error
    fwrite(STDERR,"$urlImpots[erreur]\n");
     // fin
    exit(1);
  }//si

   // análisis de la solicitud del usuario
  $demande=analyseDemande("$argv[2],$argv[3],$argv[4]");
   // ¿Errores?
  if($demande[erreur]){
       // mensaje
    echo "$demande[erreur]\n";
     // se acabó
    exit(1);
  }//si

   // se puede realizar la solicitud: se utiliza el token de sesión
  $impots=getImpots($urlImpots,$demande,$argv[5]);

   // se muestra el token de sesión
  echo "Jeton de session=[$impots[jeton]]\n";

   // ¿Hay errores?
  if(count($impots[erreurs])!=0){
       //mensajes de error
    echo "Les erreurs suivantes se sont produites :\n";
    for($i=0;$i<count($impots[erreurs]);$i++){
        echo $impots[erreurs][$i]."\n";
    }//para
  }else{
       // sin errores: visualización de las simulaciones
    echo "Simulations :\n";
    for($i=0;$i<count($impots[simulations]);$i++){
        echo "[".implode(",",$impots[simulations][$i])."]\n";
      }//para
  }//si

   // fin del programa
  exit(0);

  // --------------------------------------------------------------
  function analyseURL($URL){  
       // analiza la validez de URL $URL
    $url=parse_url($URL);
    // el protocolo    
    if(strtolower($url[scheme])!="http"){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//si
     // la máquina
    if(! isset($url[host])){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//si
     // el puerto
    if(! isset($url[port])) $url[port]=80;
     // la solicitud
    if(isset($url["query"])){
        $url[erreur]="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $url;
    }//si
     // respuesta
    return $url;
  }//analyseURL

  // -----------------------------------------------------------
  function analyseDemande($demande){
       // $demande: cadena a analizar
     // debe tener el formato «casado, hijos, salario»
    // formato válido
    if(! preg_match("/^\s*(oui|non)\s*,\s*(\d{1,3})\s*,\s*(\d+)\s*$/i",$demande,$champs)){
        // formato inválido
      return(array(erreur=>"Format (marié, enfants, salaire) invalide."));
    }    
     // está bien
    $marié=strtolower($champs[1]);
    return array(marié=>$marié,enfants=>$champs[2],salaire=>$champs[3]);
  }//analyseDemande

  // --------------------------------------------------------------
  function getImpots($urlImpots,$demande,$jeton){
       // $urlImpots: URL para consultar
     // $demande: diccionario que contiene los campos «esposa», «hijos» y «salario»
     // $jeton: un posible token de sesión

     // apertura de una conexión en el puerto $urlImpots[port] de $urlImpots[host]
    $connexion=fsockopen($urlImpots[host],$urlImpots[port],&$errno,&$erreur);
     // retorno en caso de error
    if(! $connexion){
      return array(erreurs=>array("Echec de la connexion au site ($urlImpots[host],$urlImpots[port]) : $erreur"));
    }//si

         // se envían los encabezados HTTP al servidor
    POST($connexion,$urlImpots,$jeton,
            array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));

         // se lee la respuesta del servidor: primero los encabezados HTTP
     // la primera línea
    $ligne=fgets($connexion,10000);

    // ¿Se encontró URL?
    if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){
        // URL inaccesible: se devuelve un error
      return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));
    }//si

     // se leen los demás encabezados HTTP
    while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
       // búsqueda del token si aún no se ha encontrado      
      if(! $jeton){
           // búsqueda de la línea «set-cookie»
        if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
            // se ha encontrado el token; se almacena          
          $jeton=$champs[1];            
        }//si
      }//si
    }//línea siguiente

     // se lee el documento siguiente
    $document="";
    while($ligne=fread($connexion,10000)){
        $document.=$ligne;
    }//mientras

     // se cierra la conexión
    fclose($connexion);

     // se analiza el documento
    $impots=getInfos($document);

     // se devuelve el resultado
    return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);
    }//getImpots

  // --------------------------------------------------------------
  function POST($connexion,$url,$jeton,$paramètres){
       // $connexion: conexión al servidor web
     // $url: el URL al que se debe consultar
     // $paramètres: diccionario de los parámetros que se deben enviar

     // se prepara el POST
    $post="";
    while(list($paramètre,$valeur)=each($paramètres)){
        $post.=$paramètre."=".urlencode($valeur)."&";
    }//mientras
     // se elimina el último carácter
    $post=substr($post,0,-1);

     // se prepara la solicitud HTTP en formato HTTP/1.0
    $HTTP="POST $url[path] HTTP/1.0\n";
    $HTTP.="Content-type: application/x-www-form-urlencoded\n";
    $HTTP.="Content-length: ".strlen($post)."\n";
    $HTTP.="Connection: close\n";
    if($jeton) $HTTP.="Cookie: PHPSESSID=$jeton\n";
    $HTTP.="\n";
    $HTTP.=$post;

    // se envía la solicitud HTTP
    fwrite($connexion,$HTTP);
    }//POST

  // --------------------------------------------------------------
  function getInfos($document){
       // $document: documento HTML
     // se busca, ya sea la lista de errores
     // o la tabla de simulaciones

     // preparación del resultado
    $impots[erreurs]=array();
    $impots[simulations]=array();

     // ¿Hay algún error?
     // modelo de la línea de error
    $modErreur="|<li>(.+?)</li>|";
     // búsqueda en el documento
    if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){
        // recuperación de errores
      for($i=0;$i<count($champs);$i++){
          $impots[erreurs][]=$champs[$i][1];
      }//para
       // terminado
      return $impots;
    }//si

     // modelo de una fila de la tabla de simulaciones
     // <tr><td>sí</td><td>2</td><td>200000</td><td>22504</td></tr>
    $modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";
    // búsqueda en el documento
    if(preg_match_all($modSimulation,$document,$champs,PREG_SET_ORDER)){    
         // recuperación de las simulaciones
      for($i=0;$i<count($champs);$i++){
          $impots[simulations][]=array($champs[$i][1],$champs[$i][2],$champs[$i][3],$champs[$i][4]);
      }//para
       // retorno del resultado
      return $impots;
    }//si

    // no es normal llegar hasta aquí
    return $impots;
    }//getInfos
?>

Comentarios:

  • El programa comienza por verificar la validez de los parámetros que ha recibido. Para ello, utiliza dos funciones: analyseURL, que verifica la validez de URL, y analyseDemande, que verifica los demás parámetros.
  • El cálculo del impuesto se realiza mediante la función getImpots:
  $impots=getImpots($urlImpots,$demande,$argv[5]);
  • La función getImpots se declara de la siguiente manera:
<?php
...
  // --------------------------------------------------------------
  function getImpots($urlImpots,$demande,$jeton){
       // $urlImpots: URL para consultar
     // $demande: diccionario que contiene los campos «esposa», «hijos» y «salario»
     // $jeton: un posible token de sesión
  • (continuación)
    • $urlImpots es un diccionario que contiene los siguientes campos:
      • host: máquina donde reside el servicio web
      • port: su puerto de servicio
      • path: la ruta del recurso solicitado
    • $demande es un diccionario que contiene los siguientes campos:
      • casado: sí/no: estado civil
      • hijos: número de hijos
      • salario: salario anual
    • $jeton es el token de sesión.

La función devuelve como resultado un diccionario con los siguientes campos:

  • errores: matriz de mensajes de error
  • simulaciones: matriz de simulaciones, siendo cada simulación a su vez una matriz de cuatro elementos (casado, hijos, salario, impuesto)
  • token: el token de sesión
  • Una vez obtenido el resultado de getImpots, el programa muestra los resultados y finaliza:
<?php
...

   // se muestra el token de sesión
  echo "Jeton de session=[$impots[jeton]]\n";

   // ¿Hay errores?
  if(count($impots[erreurs])!=0){
       //mensajes de error
    echo "Les erreurs suivantes se sont produites :\n";
    for($i=0;$i<count($impots[erreurs]);$i++){
        echo $impots[erreurs][$i]."\n";
    }//para
  }else{
       // sin errores: visualización de las simulaciones
    echo "Simulations :\n";
    for($i=0;$i<count($impots[simulations]);$i++){
        echo "[".implode(",",$impots[simulations][$i])."]\n";
      }//para
  }//si

   // fin del programa
  exit(0);
  • Analicemos ahora la función getImpots($urlImpots,$demande,$jeton), que debe
    • establecer una conexión TCP-IP en el puerto $urlImpots[port] de la máquina $urlImpots[host]
    • enviar al servidor web los encabezados HTTP que este espera, en particular el token de sesión, si lo hay
    • enviar al servidor web una solicitud POST con los parámetros contenidos en el diccionario $demande
    • analizar la respuesta del servidor web para encontrar en ella una lista de errores o una tabla de simulaciones.
  • El envío de los encabezados HTTP se realiza mediante la función POST:
<?php
...
         // se envían los encabezados HTTP al servidor
    POST($connexion,$urlImpots,$jeton,
            array(optMarie=>$demande[marié],txtEnfants=>$demande[enfants],txtSalaire=>$demande[salaire]));
  • Una vez enviadas las cabeceras HTTP, la función getImpots lee la respuesta completa del servidor y la almacena en $document. La respuesta se lee sin analizarla, excepto la primera línea, que nos permite saber si el servidor web encontró o no el URL que se le solicitó. De hecho, si se encontró el URL, el servidor web responde HTTP/1.X 200 OK, donde X depende de la versión de HTTP utilizada.
<?php
...
    // ¿Se encontró URL?
    if(! preg_match("/^(.+?) 200 OK\s*$/",$ligne)){
 <?php
...
       // URL inaccesible: se devuelve un error
      return array(erreurs=>array("L'URL $urlImpots[path] n'a pu être trouvée"));
    }//si
  • El documento se analiza mediante la función getInfos:
<?php
...
     // se analiza el documento
    $impots=getInfos($document);
  • El resultado es un diccionario con dos campos:
    • errores: lista de errores; puede estar vacía
    • simulaciones: lista de simulaciones —puede estar vacía—
  • Una vez hecho esto, la función getImpots puede devolver su resultado en forma de diccionario.
<?php
...
     // se devuelve el resultado
    return array(jeton=>$jeton,erreurs=>$impots[erreurs],simulations=>$impots[simulations]);
  • Ahora analicemos la función POST:
<?php
...
  // --------------------------------------------------------------
  function POST($connexion,$url,$jeton,$paramètres){
       // $connexion: la conexión al servidor web
     // $url: el URL al que se debe consultar
     // $paramètres: diccionario de los parámetros que se deben enviar

     // se prepara el POST
    $post="";
    while(list($paramètre,$valeur)=each($paramètres)){
        $post.=$paramètre."=".urlencode($valeur)."&";
    }//mientras
     // se elimina el último carácter
    $post=substr($post,0,-1);

     // se prepara la solicitud HTTP en formato HTTP/1.0
    $HTTP="POST $url[path] HTTP/1.0\n";
    $HTTP.="Content-type: application/x-www-form-urlencoded\n";
    $HTTP.="Content-length: ".strlen($post)."\n";
    $HTTP.="Connection: close\n";
    if($jeton) $HTTP.="Cookie: PHPSESSID=$jeton\n";
    $HTTP.="\n";
    $HTTP.=$post;

    // se envía la solicitud HTTP
    fwrite($connexion,$HTTP);
    }//POST

Si bien en el programa habíamos tenido la oportunidad de solicitar un recurso web mediante un GET, aún no habíamos tenido la oportunidad de hacerlo con un POST. El POST se diferencia del GET en la forma de enviar parámetros al servidor. Debe enviar los siguientes encabezados HTTP:

POST chemin HTTP/1.X
Content-type: application/x-www-form-urlencoded
Content-length: N

junto con

  • chemin: ruta del recurso web solicitado, en este caso $url[path]
  • HTTP/1.X: el protocolo HTTP deseado. Aquí se ha elegido HTTP/1.0 para recibir la respuesta en un solo bloque. HTTP/1.1 permite el envío de la respuesta en varios bloques (chunked).
  • N indica la cantidad de caracteres que el cliente está a punto de enviar al servidor

Los N caracteres que conforman los parámetros de la consulta se envían inmediatamente después de la línea en blanco que cierra los encabezados HTTP enviados al servidor. Estos parámetros tienen el formato param1=val1&param2=val2&..., donde parami es el nombre del parámetro y vali es su valor. Los valores válidos pueden contener caracteres «problemáticos», como espacios, el carácter &, el carácter =, etc. Estos caracteres deben sustituirse por una cadena %XX, donde XX es su código hexadecimal. La función PHP urlencode realiza esta tarea. La tarea inversa la realiza urldecode.

Por último, cabe señalar que, si se cuenta con un token, este se envía con un encabezado HTTP Cookie: PHPSESSID=token.

  • Nos queda por analizar la función que procesa la respuesta del servidor:
<?php
...
  // --------------------------------------------------------------
  function getInfos($document){
       // $document: documento HTML
     // se busca, ya sea la lista de errores
     // o la tabla de simulaciones

     // preparación del resultado
    $impots[erreurs]=array();
    $impots[simulations]=array();
  • Recordemos que el servidor envía los errores en el formato <li>mensaje de error</li>. La expresión regular «|<li>(.+?)</li>|» debe permitir recuperar esta información. Aquí hemos utilizado el signo | para delimitar la expresión regular en lugar del signo /, ya que este último también aparece dentro de la propia expresión regular. La función preg_match_all ($modèle, $document, $champs, PREG_SET_ORDER) permite recuperar todas las ocurrencias de $modèle que se encuentran en $document. Estas se almacenan en la tabla $champs. Así, $champs[i] representa la ocurrencia n.º i de $modèle en $document. En $champs[$i][0] se coloca la cadena correspondiente al modelo. Si este tuviera paréntesis, la cadena correspondiente al primer paréntesis se coloca en $champs[$i][1], la segunda en $champs[$i][2] y así sucesivamente. El código para recuperar los errores será, por lo tanto, el siguiente:
<?php
...

     // ¿Hay algún error?
     // modelo de la línea de error
    $modErreur="|<li>(.+?)</li>|";
     // búsqueda en el documento
    if(preg_match_all($modErreur,$document,$champs,PREG_SET_ORDER)){
        // recuperación de errores
      for($i=0;$i<count($champs);$i++){
          $impots[erreurs][]=$champs[$i][1];
      }//para
       // fin
      return $impots;
    }//si

4.7. Aplicación de impuestos: Conclusión

Hemos mostrado diferentes versiones de nuestra aplicación cliente-servidor para el cálculo de impuestos:

  • versión 1: el servicio lo proporciona un conjunto de programas PHP; el cliente es un navegador. Realiza una sola simulación y no guarda un historial de las anteriores.
  • versión 2: se agregan algunas capacidades del lado del navegador al incluir scripts de JavaScript en el documento HTML cargado por este. Controla la validez de los parámetros del formulario.
  • versión 3: se permite que el servicio recuerde las diferentes simulaciones realizadas por un cliente mediante la gestión de una sesión. La interfaz HTML se modifica en consecuencia para mostrarlas.
  • Versión 4: el cliente es ahora una aplicación de consola independiente. Esto nos permite volver a la programación de clientes web.

En este punto, cabe señalar lo siguiente:

  • Las versiones 1 a 3 admiten navegadores que solo tienen la capacidad de ejecutar scripts de JavaScript. Cabe señalar que un usuario siempre tiene la posibilidad de desactivar la ejecución de estos scripts. En ese caso, la aplicación solo funcionará parcialmente en su versión 1 (la opción «Borrar» no funcionará) y no funcionará en absoluto en sus versiones 2 y 3 (las opciones «Borrar» y «Calcular» no funcionarán). Podría ser interesante desarrollar una versión del servicio que no utilice scripts de JavaScript.

Al desarrollar un servicio web, hay que preguntarse a qué tipo de usuarios nos dirigimos. Si queremos llegar al mayor número posible de usuarios, escribiremos una aplicación que solo envíe a los navegadores HTML (sin JavaScript ni applets). Si se trabaja en una intranet y se tiene control sobre la configuración de las estaciones de trabajo de la misma, entonces se puede permitir ser más exigente en cuanto al cliente.

La versión 4 es un cliente web que extrae la información que necesita del flujo HTML enviado por el servidor. Muy a menudo no tenemos control sobre este flujo. Este es el caso cuando hemos escrito un cliente para un servicio web existente en la red y administrado por otra persona. Veamos un ejemplo. Supongamos que nuestro servicio de simulaciones de cálculo de impuestos fue desarrollado por una empresa X. Actualmente, el servicio envía las simulaciones en una tabla HTML y nuestro cliente aprovecha este hecho para recuperarlas. De esta manera, compara cada línea de la respuesta del servidor con la expresión regular:

     // plantilla de una fila de la tabla de simulaciones
     // <tr><td>sí</td><td>2</td><td>200000</td><td>22504</td></tr>
    $modSimulation="|<tr>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*</tr>|";

Supongamos ahora que el desarrollador de la aplicación cambia la apariencia visual de la respuesta, colocando las simulaciones no en una tabla, sino en una lista con el siguiente formato:

Résultat des simulations
<ul>
    <li>oui,2,200000,22504
  <li>non,2,200000,33388
</ul>  

En este caso, tendremos que reescribir nuestro cliente web. Esa es la amenaza constante que se cierne sobre los clientes web de aplicaciones que no controlamos nosotros mismos. XML puede ofrecer una solución a este problema:

  • en lugar de generar HTML, el servicio de simulaciones generará XML. En nuestro ejemplo, esto podría ser
<simulations>
    <entetes marie="marié" enfants="enfants" salaire="salaire" impot="impôt"/>
  <simulation marie="oui" enfants="2" salaire="200000" impot="22504" />
  <simulation marie="non" enfants="2" salaire="200000" impot="33388" />
</simulations>
  • Se podría asociar una hoja de estilo a esta respuesta para indicar a los navegadores el aspecto visual que debe tener esta respuesta XML
  • Los clientes web programados ignorarían esta hoja de estilo y obtendrían la información directamente del flujo XML de la respuesta

Si el desarrollador del servicio desea modificar la presentación visual de los resultados que proporciona, modificará la hoja de estilo y no el XML. Gracias a la hoja de estilo, los navegadores mostrarán la nueva presentación visual y no será necesario modificar los clientes web programados.