Skip to content

11. Ejercicio IMPOTS con un servicio WEB y una arquitectura de tres capas

Vamos a retomar el ejercicio IMPOTS (véanse los párrafos 4.2, 4.3 y 6) y lo convertiremos en una aplicación cliente/servidor. El script del servidor se dividirá en tres elementos:

  • una capa llamada [dao] (Objetos de Acceso a Datos) que se encargará de las interacciones con la base de datos MySQL
  • una capa llamada [métier] que realizará el cálculo del impuesto
  • una capa [web] que se encargará de la comunicación con los clientes web.

El script de cliente [1]:

  • transmite al script del servidor los tres datos ($marié, $enfants, $salaire) necesarios para el cálculo del impuesto
  • muestra la respuesta del servidor en la consola

El script de servidor [2] está compuesto por la capa [web] del servidor.

  • Al inicio de una nueva sesión de cliente, colocará en tablas los datos de las bases de datos MySQL y [dbimpots]. Para ello, utilizará la capa [dao]. Las tablas así creadas se colocarán en la sesión del cliente para que puedan utilizarse en futuras consultas del cliente.
  • Al recibir una solicitud del cliente, se le pasarán los tres datos ($marié, $enfants, $salaire) a la capa [métier], la cual calculará el impuesto $impot.
  • El script del servidor devolverá el impuesto $impôt calculado.

11.1. El script del cliente (clients_impots_05_web)

El script de cliente será un cliente del servicio web de cálculo del impuesto. Enviará (POST) al servidor los parámetros en el siguiente formato:

params=$casado,$hijos,$salario donde

  • $marié será la cadena oui o non,
  • $enfants será el número de hijos,
  • $salaire será el salario del contribuyente

Encuentra los tres parámetros anteriores en un archivo de texto [data.txt] en el formato (estado civil, hijos, salario):

oui,2,200000
non,2,200000
oui,3,200000
non,3,200000
oui,5,50000
non,0,3000000

El script del cliente

  • leerá el archivo de texto [data.txt] línea por línea
  • enviará la cadena params=$marié,$enfants,$salaire al servicio web de cálculo de impuestos
  • recibirá la respuesta del servicio. Esta respuesta puede presentarse de dos formas:
<erreur>message d'erreur</erreur>
<impot>montant de l'impôt</impot>
  • guardará la respuesta del servidor en un archivo de texto [resultats.txt] en una de las dos formas siguientes:
marié:enfants:salaire:erreur
marié:enfants:salaire:impôt

El código del script del cliente es el siguiente:


<?php

// cliente de impuestos
// gestión de errores
ini_set("display_errors", "off");

// ---------------------------------------------------------------------------------
// una clase de funciones de utilidad
class Utilitaires {

  function cutNewLinechar($ligne) {
    ...
  }

}

// main -----------------------------------------------------
// definición de constantes
$DATA = "data.txt";
$RESULTATS = "resultats.txt";
// datos del servidor
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/impots_05_web.php";

// los parámetros de los contribuyentes (estado civil, número de hijos, salario anual)
// se han incluido en el archivo de texto $DATA a razón de una línea por contribuyente
// los resultados (estado civil, número de hijos, salario anual, impuesto a pagar) 
// o (estado civil, número de hijos, salario anual, mensaje de error) se colocan en
// el archivo de texto $RESULTATS, a razón de un resultado por línea

// clase Utilidades
$u = new Utilitaires();

// apertura del archivo de datos de los contribuyentes
$data = fopen($DATA, "r");
if (!$data) {
  print "Impossible d'ouvrir en lecture le fichier des données [$DATA]\n";
  exit;
}

// apertura del archivo de resultados
$résultats = fopen($RESULTATS, "w");
if (!$résultats) {
  print "Impossible de créer le fichier des résultats [$RESULTATS]\n";
  exit;
}

// se procesa la línea actual del archivo de datos de los contribuyentes
while ($ligne = fgets($data, 100)) {
  // se elimina el posible marcador de fin de línea
  $ligne = $u->cutNewLineChar($ligne);
  // se recuperan los 3 campos «casado:hijos:salario» que forman $ligne
  list($marié, $enfants, $salaire) = explode(",", $ligne);
  // se calcula el impuesto
  list($erreur, $impôt) = calculerImpot($HOTE, $PORT, $urlServeur, $cookie, array($marié, $enfants, $salaire));
  // se registra el resultado
  $résultat = $erreur ? "$marié:$enfants:$salaire:$erreur" : "$marié:$enfants:$salaire:$impôt";
  fputs($résultats, "$résultat\n");
  // dato siguiente
}
// se cierran los archivos
fclose($data);
fclose($résultats);

// fin
print "Terminé...\n";
exit;

function calculerImpot($HOTE, $PORT, $urlServeur, &$cookie, $params) {
  // conecta al cliente a ($HOTE,$PORT,$urlServeur)
  //envía la cookie $cookie si esta no está vacía. $cookie se pasa por referencia
  // envía $params al servidor
  // procesa la única línea devuelta por el servidor
  
  // abre una conexión en el puerto 80 de $HOTE
  $connexion = fsockopen($HOTE, $PORT);
  // ¿Error?
  if (!$connexion)
    return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
  // los encabezados (headers) del protocolo HTTP deben terminar con una línea en blanco
  // POST
  fputs($connexion, "POST $urlServeur HTTP/1.1\n");
  // Host
  fputs($connexion, "Host: localhost\n");
  // Conexión
  fputs($connexion, "Connection: close\n");
// Se envía la cookie si no está vacía
  if ($cookie) {
    fputs($connexion, "Cookie: $cookie\n");
  }//si
  // ahora se envía la instrucción al cliente después de haberla codificado
  $infos = "params=" . urlencode(implode(",", $params));
  // se indica qué tipo de información se va a enviar
  fputs($connexion, "Content-type: application/x-www-form-urlencoded\n");
  // se envía el tamaño (número de caracteres) de la información que se va a enviar
  fputs($connexion, "Content-length: " . strlen($infos) . "\n");
  // se envía una línea en blanco
  fputs($connexion, "\n");
  // se envía la información
  fputs($connexion, $infos);
  // se muestra la respuesta del servidor web
  // y nos aseguramos de recuperar la cookie, si la hay
  while ($ligne = fgets($connexion, 1000)) {
    // cookie: solo en la primera respuesta
    if (!$cookie) {
      if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
        $cookie = $champs[1];
      }//si
    }
    // tan pronto como haya una línea en blanco, la respuesta HTTP ha terminado
    if (trim($ligne) == "") {
      break;
    }
  }//mientras
  // lectura de la línea del resultado
  $ligne = fgets($connexion, 1000);
  // se cierra la conexión
  fclose($connexion);
  // cálculo del resultado
  $erreur="";
  $impôt="";
  if (preg_match("/^<erreur>(.*?)<\/erreur>\s*$/", $ligne, $champs)) {
    $erreur = $champs[1];
  } else {
    if (preg_match("/^<impot>(.*?)<\/impot>\s*$/", $ligne, $champs)) {
      $impôt = $champs[1];
    }else{
      $erreur="résultat du serveur non exploitable";
    }
  }
  // retorno
  return array($erreur, $impôt);
}

Comentarios

El código del script de cliente incluye elementos ya vistos:

  • líneas 9-15: la clase [Utilitaires] se presentó en la versión 3, párrafo 6
  • líneas 17-68: el programa principal es similar al de la versión 1, párrafo 4.2. Solo difiere en el cálculo del impuesto, línea 56.
  • línea 56: la función de cálculo del impuesto admite los siguientes parámetros:
    • $HOTE, $PORT, $urlServeur: permiten conectarse al servicio web
    • $cookie: es la cookie de sesión. Este parámetro se pasa por referencia. Su valor lo establece la función de cálculo del impuesto. En la primera llamada, no tiene valor. A partir de ahí, sí lo tiene.
    • array($marié, $enfants, $salaire): representa una línea del archivo [data.txt]

La función de cálculo del impuesto devuelve una matriz con dos resultados ($erreur, $impôt), donde $erreur es un posible mensaje de error y $impôt el monto del impuesto.

  • líneas 70 – 134: aquí tenemos un cliente HTTP clásico, como los que hemos visto muchas veces. Cabe destacar los siguientes puntos:
  • línea 83: los parámetros ($marié, $enfants, $salaire) se transmiten al servidor mediante un POST
  • líneas 89-91: si el cliente tiene un identificador de sesión, lo envía al servidor
  • línea 93: creación del parámetro params
  • línea 101: envío del parámetro params
  • líneas 104-115: el cliente lee todos los encabezados HTTP enviados por el servidor hasta encontrar la línea vacía que marca el final de los encabezados. Aprovecha para recuperar el identificador de sesión en la respuesta a su primera solicitud.
  • líneas 123-125: se procesa una posible línea con el formato <erreur>message</erreur>
  • líneas 126-128: se hace lo mismo con una posible línea del tipo <impot>montant</impot>
  • línea 133: se devuelve el resultado

11.2. El servicio web de cálculo de impuestos

Aquí nos enfocamos en los tres scripts que componen el servidor:

El proyecto de NetBeans correspondiente es el siguiente:

En [1], el servidor está formado por los siguientes scripts PHP:

  • [impots_05_entites] contiene las clases que utiliza el servidor
  • [impots_05_dao] contiene las clases e interfaces de la capa [dao]
  • [impots_05_metier] contiene las clases e interfaces de la capa [metier]
  • [impots_05_web] contiene las clases e interfaces de la capa [dao]

Comenzamos presentando dos clases utilizadas por las diferentes capas del servicio web.

11.2.1. Las entidades del servicio web (impots_05_entites)

La base de datos MySQL [dbimpots] cuenta con una tabla [impots] que contiene los datos necesarios para el cálculo del impuesto [1]:

Almacenaremos los datos de las tablas MySQL y [impots] en un array de objetos Tranche, donde Tranche es la siguiente clase:


<?php

// un tramo impositivo
class Tranche {

  // campos privados
  private $limite;
  private $coeffR;
  private $coeffN;

  // getters y setters
  public function getLimite() {
    return $this->limite;
  }

  public function setLimite($limite) {
    $this->limite = $limite;
  }

  public function getCoeffR() {
    return $this->coeffR;
  }

  public function setCoeffR($coeffR) {
    $this->coeffR = $coeffR;
  }

  public function getCoeffN() {
    return $this->coeffN;
  }

  public function setCoeffN($coeffN) {
    $this->coeffN = $coeffN;
  }

  // constructor
  public function __construct($limite, $coeffR, $coeffN) {
    $this->setLimite($limite);
    $this->setCoeffR($coeffR);
    $this->setCoeffN($coeffN);
  }

  // toString
  public function __toString(){
    return "[$this->limite,$this->coeffR,$this->coeffN]";
  }
}

Los campos privados [$limite, $coeffR, $coeffN] se utilizarán para almacenar las columnas [limites, coeffR, coeffN] de una fila de la tabla MySQL [impots].

Además, el código del servidor utilizará una excepción propia, la clase ImpotsException:

1
2
3
4
5
6
7
class ImpotsException extends Exception {

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

}
  • línea 1: la clase [ImpotsException] deriva de la clase [Exception] predefinida en PHP 5
  • línea 3: el constructor de la clase [ImpotsException] admite dos parámetros:
    • $message: un mensaje de error
    • $code: un código de error

11.2.2. La capa [dao] (impots_05_dao)

La capa [dao] garantiza el acceso a los datos de la base de datos:

La capa [dao] presenta la siguiente interfaz:

1
2
3
4
5
6
// interfaz Dao
interface IImpotsDao {

// devuelve una matriz de entidades Tranche
  function getData();
}

La interfaz IImpotsDao solo expone la función getData. Esta función coloca en un array de objetos Tranche las diferentes filas de la tabla MySQL [dbimpots.impots].

La clase de implementación es la siguiente:


<?php

// capa DAO
// dependencias
require_once "impots_05_entites.php";
// constantes
define("TABLE", "impots");

// -----------------------------------------------------------------
// implementación abstracta
abstract class ImpotsDaoWithPdo implements IImpotsDao {

// campos privados
  private $dsn;
  private $user;
  private $passwd;
  private $tranches;

// getters y setters
  public function getDsn() {
    return $this->dsn;
  }

  public function setDsn($dsn) {
    $this->dsn = $dsn;
  }

  public function getUser() {
    return $this->user;
  }

  public function setUser($user) {
    $this->user = $user;
  }

  public function getPasswd() {
    return $this->passwd;
  }

  public function setPasswd($passwd) {
    $this->passwd = $passwd;
  }

  // constructor
  public function __construct($dsn, $user, $passwd) {
    // se registran los parámetros
    $this->setDsn($dsn);
    $this->setUser($user);
    $this->setPasswd($passwd);
    // se recuperan los datos de SGBD
    // conecta ($user, $pwd) a la base de datos $dsn
    try {
      // conexión
      $connexion = new PDO($dsn, $user, $passwd, array(PDO::ATTR_PERSISTENT => true));
      // lee la tabla $TABLE
      $requête = "select limites,coeffR,coeffN from " . TABLE;
      // ejecuta la consulta $requête en la conexión $connexion
      $statement = $connexion->prepare($requête);
      $statement->execute();
      // procesamiento del resultado de la consulta
      while ($colonnes = $statement->fetch()) {
        $this->tranches[] = new Tranche($colonnes[0], $colonnes[1], $colonnes[2]);
      }
      // desconexión
      $connexion=NULL;
    } catch (PDOException $e) {
      // regreso con error
      throw new ImpotsException($e->getMessage(), 1);
    }
  }

  public function getData(){
    return $this->tranches;
  }
}
  • línea 5: la implementación de la interfaz [IImpotsDao] requiere las clases definidas en el script [impots_05_entites].
  • línea 11: definición de una clase abstracta. Una clase abstracta es una clase de la que no se pueden crear instancias. Una clase abstracta debe derivarse obligatoriamente para poder crear instancias de ella. Una clase puede declararse abstracta porque no se puede instanciar (algunos de sus métodos no están definidos) o porque no se desea instanciarla. En este caso, no se desea instanciar la clase [ImpotsDaoWithPdo]. Se instanciarán clases derivadas.
  • línea 11: la clase [ImpotsDaoWithPdo] implementa la interfaz [IImpotsDao]. Por lo tanto, debe definir el método getData. Este método se encuentra en las líneas 72-74.
  • línea 14: $dsn (Data Source Name) es una cadena de caracteres que identifica de manera única a SGBD y a la base de datos utilizada.
  • línea 15: $user identifica al usuario que se conecta a la base de datos
  • línea 16: $passwd es la contraseña del usuario anterior
  • línea 17: $tranches es la matriz de objetos Tranche en la que se almacenarán las tablas MySQL y [dbimpots.impots].
  • líneas 45-70: el constructor de la clase. Este código ya se ha visto en la versión 4, párrafo 8.2. Cabe señalar que la construcción del objeto [ImpotsDaoWithPdo] puede fallar. En ese caso, se lanza una excepción de tipo [ImpotsException].
  • líneas 72-74: el método [getData] de la interfaz [IImpotsDao].

La clase [ImpotsDaoWithPdo] es compatible con cualquier SGBD. El constructor de la clase, en la línea 45, requiere conocer el nombre de la fuente de datos (Data Source Name) de la base de datos. Esta cadena de caracteres depende del SGBD utilizado. Se ha decidido no exigir al usuario de la clase que conozca este nombre de fuente de datos. Para cada SGBD, habrá una clase específica derivada de [ImpotsDaoWithPdo]. Para el SGBD y el MySQL, será la siguiente clase:


class ImpotsDaoWithMySQL extends ImpotsDaoWithPdo {

  public function __construct($host, $port, $base, $user, $passwd) {
    parent::__construct("mysql:host=$host;dbname=$base;port=$port", $user, $passwd);
  }

}
  • En la línea 3, el desarrollador no solicita el nombre de la fuente de datos (Data Source Name), sino simplemente el nombre de la máquina host de SGBD ($host), su puerto de escucha ($port) y el nombre de la base de datos ($base).
  • En la línea 4, se crea el nombre de la fuente de datos (Data Source Name) de la base de datos MySQL y se utiliza para llamar al constructor de la clase padre.

Cabe señalar que, para adaptarse a otro SGBD, basta con escribir la clase derivada de [ImpotsDaoWithPdo] que corresponda. En cada caso, se trata de construir el nombre de la fuente de datos específico del SGBD utilizado.

11.2.3. La capa [métier] (impots_05_metier)

La capa [metier] contiene la lógica para el cálculo del impuesto:

La capa [métier] presenta la siguiente interfaz:


<?php

// interfaz de negocio
interface IImpotsMetier {
  public function calculerImpot($marié, $enfants, $salaire);
}

La interfaz [IImpotsMetier] solo expone un método, el método [calculerImpot], que permite calcular el impuesto de un contribuyente a partir de los siguientes parámetros:

  • $marié: cadena «sí» o «no», dependiendo de si el contribuyente está casado o no
  • $enfants: el número de hijos del contribuyente
  • $salaire: su salario

La capa [web] le proporcionará estos parámetros.

La implementación de la interfaz [IImpotsMetier] es la siguiente:


// dependencias
require_once "impots_05_dao.php";

// ------------------------------------------------------------------
// clase de implementación
class ImpotsMetier implements IImpotsMetier {

  // capa DAO
  private $dao;
   // matriz de objetos [Tranche]
  private $data;

  // getter y setter
  public function getDao() {
    return $this->dao;
  }

  public function setDao($dao) {
    $this->dao = $dao;
  }

  public function setData($data){
    $this->data=$data;
  }
  
  public function __construct($dao) {
    // se recuperan los datos necesarios para el cálculo del impuesto
    $this->setDao($dao);
    $this->setData($this->dao->getData());
  }

  public function calculerImpot($marié, $enfants, $salaire) {
    // $marié: sí, no
    // $enfants: número de hijos
    // $salaire: salario anual
    // número de partes
    $marié = strtolower($marié);
    if ($marié == "oui")
      $nbParts = $enfants / 2 + 2;
    else
      $nbParts=$enfants / 2 + 1;
    // media participación adicional si hay al menos tres hijos
    if ($enfants >= 3)
      $nbParts+=0.5;
    // ingresos imponibles
    $revenuImposable = 0.72 * $salaire;
    //: coeficiente familiar
    $quotient = $revenuImposable / $nbParts;
    // se coloca al final de la tabla de límites para detener el bucle siguiente
    $N = count($this->data);
    $this->data[$N - 1]->setLimite($quotient);
    // cálculo del impuesto
    $i = 0;
    while ($i < $N and $quotient > $this->data[$i]->getLimite()) {
      $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->data[$i]->getCoeffR() - $nbParts * $this->data[$i]->getCoeffN());
  }

}
  • línea 2: la capa [métier] necesita las clases de la capa [dao] y las entidades (Tranche, ImpotsException).
  • línea 6: la clase [ImpotsMetier] implementa la interfaz [IimpotsMetier].
  • líneas 9-11: los campos privados de la clase:
    • $dao: referencia a la capa [dao]
    • $data: matriz de objetos de tipo [Tranche] proporcionada por la capa [dao]
  • líneas 26-30: el constructor de la clase inicializa los dos campos anteriores. Recibe como parámetro una referencia a la capa [dao].
  • líneas 32-61: implementación del método [calculerImpot] de la interfaz [IimpotsMetier]. Este método ya aparecía en la versión 1 (párrafo 4.2).

11.2.4. La capa [web] (impots_05_web)

La capa [metier] contiene la lógica para el cálculo del impuesto:

La capa [web] está compuesta por el servicio web que responde a los clientes web. Cabe recordar que estos envían una solicitud al servicio web al enviar el siguiente parámetro: params=marié,enfants,salaire. Se trata de un servicio web similar a los que hemos podido construir en los párrafos anteriores. Su código es el siguiente:


<?php

// capa de negocio
require_once "impots_05_metier.php";

// gestión de errores
ini_set("display_errors", "off");

// encabezado UTF-8
header("Content-Type: text/plain; charset=utf-8");

// ------------------------------------------------------------------------------
// el servicio web de impuestos
// definición de constantes
$HOTE = "localhost";
$PORT = 3306;
$BASE = "dbimpots";
$USER = "root";
$PWD = "";
// los datos necesarios para el cálculo del impuesto se han colocado en la tabla MySQL IMPOTS
// que pertenece a la base de datos $BASE. La tabla tiene la siguiente estructura
// límites decimal(10,2), coeffR decimal(6,2), coeffN decimal(10,2)
// los parámetros de las personas sujetas a impuestos (estado civil, número de hijos, salario anual)
// son enviados por el cliente en el formato params=estado civil, número de hijos, salario anual
// los resultados (estado civil, número de hijos, salario anual, impuesto a pagar) se devuelven al cliente
// en el formato <impuesto>impuesto</impuesto>
// o en el formato <error>error</error>, si los parámetros no son válidos

// se recupera la capa [métier] en la sesión
session_start();
if (!isset($_SESSION['metier'])) {
// instanciación de la capa [dao] y de la capa [métier]
  try {
    $_SESSION['metier'] = new ImpotsMetier(new ImpotsDaoWithMySQL($HOTE, $PORT, $BASE, $USER, $PWD));
  } catch (ImpotsException $ie) {
    print "<erreur>Erreur : " . utf8_encode($ie->getMessage() . "</erreur>");
    exit;
  }
}
$metier = $_SESSION['metier'];

// se recupera la línea enviada por el cliente
$params = utf8_encode(htmlspecialchars(strtolower(trim($_POST['params']))));
$items = explode(",", $params);
// solo debe haber 3 parámetros
if (count($items) != 3) {
  print "<erreur>[$params] : nombre de paramètres invalides</erreur>\n";
  exit;
}//si
// el primer parámetro (estado civil) debe ser sí/no
$marié = trim($items[0]);
if ($marié != "oui" and $marié != "non") {
  print "<erreur>[$params] : 1er paramètre invalide</erreur>\n";
  exit;
}//si
// el segundo parámetro (número de hijos) debe ser un número entero
if (!preg_match("/^\s*(\d+)\s*$/", $items[1], $champs)) {
  print "<erreur>[$params] : 2ième paramètre invalide</erreur>\n";
  exit;
}//si
$enfants = $champs[1];
// el tercer parámetro (salario) debe ser un número entero
if (!preg_match("/^\s*(\d+)\s*$/", $items[2], $champs)) {
  print "<erreur>[$params] : 3ième paramètre invalide</erreur>\n";
  exit;
}//si
$salaire = $champs[1];
// se calcula el impuesto
$impôt = $metier->calculerImpot($marié, $enfants, $salaire);
// se devuelve el resultado
print "<impot>$impôt</impot>\n";
// fin
exit;
  • línea 4: la capa [web] necesita las clases de la capa [métier]
  • líneas 30-40: la referencia a la capa [métier] se activa en la sesión. Si recordamos que esta capa [métier] tiene una referencia a la capa [dao] y que esta última almacena los datos de la SGBD, entonces se entiende que:
    • que la primera solicitud del cliente provocará un acceso a SGBD
    • que las solicitudes siguientes del mismo cliente utilizarán los datos almacenados por la capa [dao]. Por lo tanto, no hay acceso a la capa SGBD.
  • línea 34: construcción de una capa [métier] que funciona con una capa [dao] implementada para SGBD MySQL
  • líneas 35-37: manejo de un posible error en la operación anterior. En este caso, se envía una línea <erreur>message</erreur> al cliente.
  • línea 43: se recupera el parámetro «params» que fue enviado por el cliente.
  • líneas 46-49: se verifica el número de datos encontrados en «params»
  • líneas 51-55: se verifica la validez de la primera información
  • líneas 56-60: lo mismo para la segunda
  • líneas 62-66: lo mismo para la tercera
  • línea 69: es la capa [métier] la que calcula el impuesto.
  • línea 71: envío del resultado al cliente

Resultados

Recordemos que el cliente [client_impots_web_05] procesa el siguiente archivo [data.txt]:

oui,2,200000
non,2,200000
oui,3,200000
non,3,200000
oui,5,50000
non,0,3000000

A partir de estas líneas (estado civil, hijos, salario), el cliente consulta el servidor de cálculo de impuestos y registra los resultados en el archivo de texto [resultats.txt]. Tras la ejecución del cliente, el contenido de este archivo es el siguiente:

oui:2:200000:22504
non:2:200000:33388
oui:3:200000:16400
non:3:200000:22504
oui:5:50000:0
non:0:3000000:1354938

donde cada línea tiene el formato (casado, hijos, salario, impuesto calculado).