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
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:
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

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

- 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
- 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

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:
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 | |
el número de hijos del contribuyente | |
su salario anual | |
el monto del impuesto a pagar | |
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:

Se ingresan datos incorrectos en el formulario:

Por último, se ingresan valores correctos:

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:

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:

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:
- 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:
- 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.
- $urlImpots es un diccionario que contiene los siguientes campos:
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:
- 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:
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¶m2=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:
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.
