Skip to content

3. Introducción a la programación web en PHP

3.1. Programación PHP

Cabe recordar aquí que PHP es un lenguaje en sí mismo y que, si bien se utiliza principalmente en el desarrollo de aplicaciones web, también puede emplearse en otros contextos. El documento «PHP a través de ejemplos», disponible en http://shiva.istia.univ-angers.fr/~tahe/pub/php/php.pdf, presenta los fundamentos del lenguaje. Aquí se da por sentado que ya se conocen dichos conceptos. Veamos, con un ejemplo sencillo, el procedimiento para ejecutar un programa PHP en Windows. El siguiente código se ha guardado con el nombre coucou.php.

<?php
    echo "coucou";
?>

Este programa se ejecuta en una ventana de DOS de Windows:


dos>"e:\program files\easyphp\php\php.exe" coucou.php
X-Powered-By: PHP/4.3.0-dev
Content-type: text/html

coucou

Cabe señalar que el intérprete PHP envía por defecto:

  • el intérprete PHP es php.exe y normalmente se encuentra en el directorio <php> de instalación del software.
  • los encabezados HTTP X-Powered-By y Content-type:
  • la línea en blanco que separa los encabezados HTTP del resto del documento
  • el documento formado aquí por el texto generado por la función echo

3.2. El archivo de configuración del intérprete PHP

El comportamiento del intérprete PHP se configura mediante un archivo de configuración llamado php.ini, el cual, en Windows, se encuentra en el propio directorio de Windows. Se trata de un archivo de tamaño considerable, ya que en Windows y para la versión 4.2 de PHP, tiene cerca de 1000 líneas, de las cuales, afortunadamente, tres cuartas partes son comentarios. Analicemos algunos de los atributos de configuración de PHP:

short_open_tag = On
permite incluir instrucciones entre las etiquetas <? >. En off, habría que incluirlas entre <?php ... >
asp_tags = Off
en on permite utilizar la sintaxis <% =variable %> que usa la tecnología ASP (Active Server Pages)
expose_php = On
permite enviar el encabezado HTTP X-Powered-By: PHP/4.3.0-dev. En off, este encabezado se elimina.
error_reporting  =  E_ALL & ~E_NOTICE
establece el alcance del seguimiento de errores. Aquí se reportarán todos los errores (E_ALL), excepto las advertencias durante la ejecución (~E_NOTICE)
display_errors = Off
en on, coloca los errores en el flujo HTML que se envía al cliente. Por lo tanto, estos se visualizan en el navegador. Se recomienda configurar esta opción en off.
log_errors = On
los errores se guardarán en un archivo
track_errors = On
almacena el último error ocurrido en la variable $php_errormsg
error_log = E:\Program Files\EasyPHP\php\erreurs.log
establece el archivo de almacenamiento de errores (si log_errors=on)
register_globals = Off
En on, varias variables se vuelven globales. Se considera una vulnerabilidad de seguridad.
default_mimetype = "text/html"
Genera por defecto el encabezado HTTP: Content-type: text/html
include_path    =".;E:\Program Files\EasyPHP\php\pear\"
la lista de directorios que se explorarán en busca de los archivos requeridos por las directivas include o require
session.save_path = /temp
el directorio donde se guardarán los archivos que almacenan las diferentes sesiones en curso. El disco en cuestión es aquel en el que se instaló PHP. Aquí, /temp se refiere a e:\temp

Este archivo de configuración influye en la portabilidad del programa PHP escrito. De hecho, si una aplicación web necesita recuperar el valor de un campo C de un formulario web, podrá hacerlo de diversas maneras, dependiendo de si la variable de configuración register_globals tiene el valor on o off:

  • off: el valor se recuperará mediante $HTTP_GET_VARS["C"] o _GET["C"] o $HTTP_POST_VARS["C"] o $_POST["C"], según el método (GET/POST) que utilice el cliente para enviar los valores del formulario
  • : igual que arriba, más $C, ya que el valor del campo C se ha convertido en global en una variable que lleva el mismo nombre que el campo

Si un desarrollador escribe un programa utilizando la notación $C porque el servidor web/PHP que utiliza asigna a la variable register_globals el valor de on, ese programa ya no funcionará si se traslada a un servidor web/PHP donde esa misma variable sea off. Por lo tanto, se buscará escribir programas evitando el uso de funcionalidades que dependan de la configuración del servidor web/PHP.

3.3. Configurar PHP en tiempo de ejecución

Para mejorar la portabilidad de un programa PHP, se pueden establecer manualmente algunas de las variables de configuración de PHP. Estas se modifican durante la ejecución del programa y solo para él. Hay dos funciones útiles en este proceso:

ini_get("confVariable")
devuelve el valor de la variable de configuración confVariable
ini_set("confVariable","valeur")
establece el valor de la variable de configuración confVariable

A continuación se muestra un ejemplo en el que se establece el valor de la variable de configuración track_errors:

<?php
     // valor de la variable de configuración track_errors
    echo "track_errors=".ini_get("track_errors")."\n";
  // cambio de este valor
  ini_set("track_errors","off");
   // verificación
  echo "track_errors=".ini_get("track_errors")."\n";
?>

Al ejecutarlo, se obtienen los siguientes resultados:

E:\data\serge\web\php\poly\intro>"E:\Program Files\EasyPHP\php\php.exe" conf1.php
Content-type: text/html

track_errors=1
track_errors=off

El valor de la variable de configuración track_errors era inicialmente 1 (~on). Lo cambiamos a off. Cabe destacar que, si nuestra aplicación debe basarse en ciertos valores de las variables de configuración, es recomendable inicializarlas dentro del propio programa.

3.4. Contexto de ejecución de los ejemplos

Los ejemplos de este folleto se ejecutarán con la siguiente configuración:

  • PC en Windows 2000
  • servidor Apache 1.3
  • PHP 4.3

La configuración del servidor Apache se realiza en el archivo httpd.conf. Las siguientes líneas le indican a Apache que cargue PHP como un módulo integrado en Apache y que pase al intérprete PHP cualquier solicitud dirigida a un documento con ciertas extensiones, entre ellas .php. Este es el sufijo predeterminado que utilizaremos para nuestros programas PHP.

LoadModule php4_module "E:/Program Files/EasyPHP/php/php4apache.dll"
AddModule mod_php4.c
AddType application/x-httpd-php .phtml .pwml .php3 .php4 .php .php2 .inc

Además, hemos definido un alias «poly» para Apache:

Alias "/poly/" "e:/data/serge/web/php/poly/"
<Directory "e:/data/serge/web/php/poly">
  Options Indexes FollowSymLinks Includes
  AllowOverride All
   #Orden: permitir, denegar
  Allow from all
</Directory>

Llamemos <poly> a la ruta e:/data/serge/web/php/poly. Si queremos solicitar con un navegador el documento doc.php al servidor Apache, utilizaremos la URL URL http://localhost/poly/doc.php. El servidor Apache reconocerá en el URL el alias poly y, por lo tanto, asociará el URL /poly/doc.php con el documento <poly>\doc.php.

3.5. Un primer ejemplo

Escribamos una primera aplicación web/PHP. El siguiente texto se guarda en el archivo heure.php:

<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php
              $maintenant=time();
              echo date("j/m/y, h:i:s",$maintenant);
            ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Si solicitamos esta página con un navegador, obtenemos el siguiente resultado:

Image

La parte dinámica de la página fue generada por el código PHP:

            <?php
              $maintenant=time();
              echo date("j/m/y, h:i:s",$maintenant);
            ?>

¿Qué pasó exactamente? El navegador solicitó el archivo URL http://localhost/poly/intro/heure.php. El servidor web (en este caso, Apache) recibió esta solicitud y, debido a la extensión .php del documento solicitado, detectó que debía reenviar esta solicitud al intérprete PHP. Este, a su vez, analiza el documento heure.php y ejecuta todas las secciones de código ubicadas entre las etiquetas <?php > y reemplaza cada una de ellas por las líneas escritas por las instrucciones PHP, echo o print. De este modo, el intérprete PHP ejecutará la parte de código anterior y la reemplazará por la línea generada por la instrucción echo:

2/10/02, 06:13:47

Una vez que se han ejecutado todas las partes de código PHP, el documento PHP se convierte en un simple documento HTML, el cual se envía al cliente.

Se buscará evitar al máximo mezclar el código PHP con el código HTML. Con este fin, se podría reescribir la aplicación anterior de la siguiente manera:

<!-- código PHP -->
<?php
     // se obtiene la hora actual
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- código HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

El resultado que se obtiene en el navegador es idéntico:

Image

La segunda versión es mejor que la primera porque hay menos código PHP en el código HTML. De esta manera, la estructura de la página es más visible. Se puede ir más allá colocando el código PHP y el código HTML en dos archivos diferentes. El código PHP se almacena en el archivo heure3.php:

<!-- código PHP -->
<?php
     // se obtiene la hora actual
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
   // se muestra la respuesta
  include "heure3-page1.php";
?>

El código HTML se almacena en el archivo heure3-page1.php:

<!-- código HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

Cuando el navegador solicite el documento heure3.php, este será cargado y analizado por el intérprete PHP. Al encontrar la línea

  include "heure3-page1.php";

El intérprete incluirá el archivo heure3-page1.php en el código fuente de heure3.php y lo ejecutará. De este modo, todo ocurre como si tuviéramos el siguiente código PHP:

<!-- código PHP -->
<?php
     // se obtiene la hora actual
    $maintenant=time();
    $maintenant=date("j/m/y, h:i:s",$maintenant);
?>
<!-- código HTML -->
<html>
  <head>
    <title>Une page php dynamique</title>
   </head>
   <body>
     <center>
     <h1>Une page PHP générée dynamiquement</h1>
     <h2>
            <?php echo  $maintenant ?>
     </h2>
     <br>
     A chaque fois que vous rafraîchissez la page, l'heure change.
   </body>
</html>

El resultado obtenido es el mismo que antes:

Image

Posteriormente se adoptará la solución de colocar los códigos PHP y HTML en archivos diferentes. Esto presenta varias ventajas:

  • la estructura de las páginas enviadas al cliente no queda oculta en el código PHP. De este modo, pueden ser mantenidas por un «diseñador web» con habilidades gráficas, pero con pocos conocimientos de PHP.
  • El código PHP actúa como «interfaz» para las solicitudes de los clientes. Su objetivo es calcular los datos necesarios para la página que se enviará como respuesta al cliente.

Sin embargo, la solución presenta una desventaja: en lugar de requerir la carga de un solo documento, requiere la carga de varios documentos, lo que puede provocar una pérdida de rendimiento.

3.6. Recuperar los parámetros enviados por un cliente web

3.6.1. mediante un POST

Consideremos el siguiente formulario en el que el usuario debe proporcionar dos datos: un nombre y una edad.

Image

Cuando el usuario ha llenado los campos Nom y Age, presiona el botón Envoyer, que es de tipo submit. Los valores del formulario se envían entonces al servidor. Este devuelve el formulario junto con una tabla que enumera los valores que ha recibido:

Image

El navegador solicita el formulario a la siguiente aplicación nomage.php:

<?php
   // ¿Tenemos los parámetros esperados?
  $post=isset($_POST["txtNom"]) && isset($_POST["txtAge"]);
  if($post){
    // se recuperan los parámetros txtNom y txtAge «enviados» por el cliente
    $nom=$_POST["txtNom"];
    $age=$_POST["txtAge"];
  } else {
      $nom="";
    $age="";
  }//si
   // se muestra la página
  include "nomage-p1.php";
?>

Algunas explicaciones:

  • un campo de formulario HTML, denominado «campo», puede enviarse al servidor mediante el método GET o el método POST. Si se envía mediante el método GET, el servidor puede recuperarlo en la variable $_GET["champ"] y en la variable $_POST["champ"] sisi se envía mediante el método POST.
  • La existencia de un dato se puede verificar con la función isset(dato), que devuelve true si el dato existe, y false en caso contrario.
  • La aplicación nomage.php crea tres variables: $nom para el nombre del formulario, $age para la edad y $post para indicar si se han «enviado» valores o no. Estas tres variables se transmiten a la página nomage-p1.php. Cabe señalar que, si bien esta participa en la elaboración de la respuesta al cliente, este no tiene conocimiento de ello. Para él, es la aplicación nomage.php la que le responde.
  • La primera vez que un cliente solicita la aplicación nomage.php, se devuelve $post como falsa. De hecho, durante esta primera llamada, no se transmite ningún valor del formulario al servidor.

La página nomage-p1.php es la siguiente:

<html>
  <head>
    <title>Formulaire web</title>
  </head>

   <body>
     <center>
       <h3>Un formulaire Web</h3>
       <h4>Récupération des valeurs des champs d'un formulaire</h4>
       <hr>
       <form name="frmPersonne" method="post">
         <table>
           <tr>
             <td>Nom</td>
             <td><input type="text" value="<?php echo $nom ?>" name="txtNom" size="20"></td>
             <td>Age</td>
             <td><input type="text" value="<?php echo $age ?>" name="txtAge" size="3"></td>
           <tr>
         </table>
         <input type="submit" name="cmdEffacer" value="Envoyer">
       </form>
     </center>
     <hr>
     <?php
          // ¿Se han enviado valores? 
         if ($post) { 
     ?>
         <h4>Valeurs récupérées</h4>
      <table border="1">
          <tr>
            <td>Nom</td><td><?php echo $nom ?></td>
          <td width="10"></td>
            <td>Age</td><td><?php echo $age ?></td>
        <tr>
      </table>
    <?php } ?>
   </body>
</html>

La aplicación nomage-p1.php presenta el formulario frmPersonne. Este está definido por la etiqueta:

       <form name="frmPersonne" method="post">

Dado que el atributo action de la etiqueta no está definido, el navegador enviará los datos del formulario a la aplicación URL a la que consultó para obtenerlo, c.a.d. la aplicación nomage.php.

Distinguamos los dos casos de llamada a la aplicación nomage.php:

  1. Es la primera vez que el usuario la invoca. Por lo tanto, la aplicación nomage.php invoca a la aplicación nomage-p1.php proporcionándole los valores ($nom,$age,$post)=("","",falso). La aplicación nomage-p1.php muestra entonces un formulario vacío.
  2. El usuario llena el formulario y utiliza el botón Envoyer (del tipo submit). Los valores del formulario (txtNom, txtAge) se «envían» (method="post" en <form>) a la aplicación nomage.php (el atributo action no está definido en <form>). La aplicación nomage.php calcula ($nom, $age,$post) = (txtNom, txtAge, verdadero) y los transmite a la aplicación nomage-p1.php, la cual muestra entonces un formulario ya completado, así como la tabla de los valores recuperados.

3.6.2. mediante un GET

En el caso de que los valores del formulario se transmitan al servidor mediante un GET, la aplicación nomage.php se convierte en la siguiente aplicación nomage2.php:

<?php
   // ¿Se tienen los parámetros esperados?
  $get=isset($_GET["txtNom"]) && isset($_GET["txtAge"]);
  if($get){
     //: el cliente recupera los parámetros txtNom y txtAge «GETTés»
    $nom=$_GET["txtNom"];
    $age=$_GET["txtAge"];
  } else {
      $nom="";
    $age="";
  }//si
   // se muestra la página
  include "nomage-p2.php";
?>

La aplicación nomage-p2.php es idéntica a la aplicación nomage-p1.php, salvo por los siguientes detalles:

  • se modifica la etiqueta form:
       <form name="frmPersonne" method="get">
  • la aplicación ahora recupera una variable $get en lugar de $post:
     <?php
          // ¿Se enviaron valores al servidor? 
         if ($get) { 
     ?>

Al ejecutarse, cuando se ingresan valores en el formulario y se envían al servidor, el navegador refleja en su campo URL el hecho de que los valores se enviaron mediante el método GET:

Image

3.7. Recuperar los encabezados HTTP enviados por un cliente web

Cuando un navegador realiza una solicitud a un servidor web, le envía una serie de encabezados HTTP. En ocasiones, resulta interesante tener acceso a ellos. Para empezar, podemos valernos de la tabla asociativa $_SERVER. Este contiene diversa información proporcionada por el servidor web, entre la que se incluyen los encabezados HTTP proporcionados por el cliente. Consideremos el siguiente programa que muestra todos los valores de la tabla $_SERVER:

<?php
     // muestra las variables relacionadas con el servidor web
   // se envía texto simple
  header("Content-type: text/plain");
   // recorrido por el array asociativo $_SERVER
     reset($_SERVER);
     while (list($clé,$valeur)=each($_SERVER)){
      echo "$clé : $valeur\n";
    }//while
?>

Guardemos este código en headers.php y solicitemos este URL con un navegador:

Image

Recuperamos cierta información, entre la que se encuentran los encabezados HTTP enviados por el navegador. Estos son los valores asociados a las claves que comienzan con HTTP. Detallamos parte de la información obtenida anteriormente:

HTTP_ACCEPT
tipos de documentos aceptados por el cliente web
HTTP_ACCEPT_CHARSET
tipos de caracteres aceptados en los documentos
HTTP_ACCEPT_ENCODING
Tipos de codificaciones aceptadas para los documentos
HTTP_ACCEPT_LANGUAGE
Tipos de idiomas aceptados para los documentos
HTTP_CONNECTION
Tipo de conexión con el servidor. Keep-Alive: el servidor debe mantener la conexión abierta una vez que haya dado su respuesta
HTTP_KEEP_ALIVE
? Duración máxima de la conexión
HOST
máquina host consultada por el cliente
HTTP_USER_AGENT
identidad del cliente
REMOTE_ADDR
Dirección del cliente IP
REMOTE_PORT
puerto de comunicación utilizado por el cliente
SERVER_PROTOCOL
protocolo HTTP utilizado por el servidor
REQUEST_METHOD
método de consulta utilizado por el cliente (GET o POST)
QUERY_STRING
solicitud ?param1=val1&param2=val2&... ubicada detrás de la solicitud URL (método GET)

Al modificar ligeramente el código del programa anterior, podemos recuperar únicamente los encabezados HTTP:

<?php
     // muestra las variables relacionadas con el servidor web
   // se envía texto simple
  header("Content-type: text/plain");
   // recorrido por la tabla asociativa $_SERVER
     reset($_SERVER);
     while (list($clé,$valeur)=each($_SERVER)){
      // ¿Encabezado HTTP?
    if(strtolower(substr($clé,0,4))=="http")
          echo substr($clé,5)." : $valeur\n";
    }//mientras
?>

El resultado que se obtiene en el navegador es el siguiente:

Image

Si se desea un encabezado HTTP específico, se escribirá, por ejemplo, $_SERVER["HTTP_ACCEPT"].

3.8. Obtener información del entorno

El servidor web/PHP se ejecuta en un entorno del que podemos obtener información a través de la tabla $_ENV, que almacena diversas características del entorno de ejecución. Consideremos la siguiente aplicación env1.php:

<?php
     // muestra las variables relacionadas con el servidor web
   // se envía texto simple
  header("Content-type: text/plain");
   // recorre el array asociativo $_ENV
     reset($_ENV);
     while (list($clé,$valeur)=each($_ENV)){
      echo "$clé : $valeur\n";
    }//mientras
?>

Muestra el siguiente resultado en un navegador (vista parcial):

Image

Como se puede ver en el ejemplo anterior, el servidor web/PHP se ejecuta en Windows OS NT.

3.9. Ejemplos

3.9.1. Generación dinámica de formularios - 1

Tomamos como ejemplo la generación de un formulario que solo tiene un control: un menú desplegable. El contenido de este menú se construye dinámicamente con valores tomados de una tabla. En la práctica, estos valores suelen obtenerse de una base de datos. El formulario es el siguiente:

Image

Si en el ejemplo anterior se ejecuta Envoyer, se obtiene la siguiente respuesta:

Image

El código HTML del formulario inicial, una vez generado, es el siguiente:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <option>un</option>
<option>deux</option>
<option>trois</option>
<option>quatre</option>
<option>cinq</option>
<option>six</option>
<option>sept</option>
<option>huit</option>
<option>neuf</option>
<option>dix</option>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
  </body>
</html>

La aplicación PHP consta de una página principal, valeurs.php, que se invoca tanto para obtener el formulario inicial (la lista de valores) como para procesar los valores de este y proporcionar la respuesta (el valor seleccionado). La aplicación genera dos páginas diferentes:

  • la del formulario inicial, que será generada por el programa valeurs-p1.php
  • la de la respuesta que se le da al usuario, que será generada por el programa valeurs-p2.php

La aplicación valeurs.php es la siguiente:

<?php

     // el array de valores
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

     // ¿Se tienen los parámetros esperados?
  $requêteVide=! isset($_POST["cmbValeurs"]);

   // se recupera la elección del usuario
  if ($requêteVide){
       // solicitud inicial
    include "valeurs-p1.php";
  }else{
       // respuesta a un POST 
      $choix=$_POST["cmbValeurs"];
      include "valeurs-p2.php";    
  }
?>

Define la tabla de valores y llama a valeurs-p1.php para generar el formulario inicial si la solicitud del cliente estaba vacía, o a valeurs-p2.php para generar la respuesta si se tenía una solicitud válida. El programa valeurs1-php es el siguiente:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
            for($i=0;$i<count($valeurs);$i++){
              echo "<option>$valeurs[$i]</option>\n";
          }//para
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
  </body>
</html>

La lista de valores del menú desplegable se genera dinámicamente a partir de la tabla $valeurs transmitida por valeurs.php. El programa valeurs-p2.php genera la respuesta:

<html>
    <head>
      <title>réponse</title>
  </head>
  <body>
      <h2>Vous avez choisi le nombre <?php echo $choix ?></h2>
  </body>
</html>

Aquí solo se muestra el valor de la variable $choix, que también se transmite mediante valeurs.php.

3.9.2. Generación dinámica de formularios - 2

Retomamos el ejemplo anterior y lo modificamos de la siguiente manera. El formulario propuesto sigue siendo el mismo:

Image

¿Es diferente la respuesta?

Image

En la respuesta, se devuelve el formulario, con el número elegido por el usuario indicado debajo. Además, este número es el que aparece como seleccionado en la lista que muestra la respuesta.

El código de valeurs.php es el siguiente:

<?php

     // configuración
  ini_set("register_globals","off");

    // la tabla de valores
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

   // se recupera la selección del usuario, si la hay
  $choix=$_POST["cmbValeurs"];

   // se muestra la respuesta  
  include "valeurs-p1.php";
?>

Cabe señalar que aquí nos hemos asegurado de configurar PHP para que no haya variables globales. Por lo general, esta es una precaución sensata, ya que las variables globales plantean problemas de seguridad. Una alternativa es inicializar todas las variables que se utilizan. Esto tendrá como efecto «sobrescribir» cualquier variable global que tenga el mismo nombre.

La página del formulario se muestra mediante valeurs-p1.php:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
            for($i=0;$i<count($valeurs);$i++){
              // si la opción actual es igual a la selección, se selecciona
              if (isset($choix) && $choix==$valeurs[$i])
                  echo "<option selected>$valeurs[$i]</option>\n";
            else echo "<option>$valeurs[$i]</option>\n";
          }//para
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
    <?php
         // continúa en la página siguiente
      if(isset($choix)){
          echo "<hr>\n";
          echo "<h3>Vous avez choisi le nombre $choix</h3>\n";
      }
    ?>
  </body>
</html>

El programa generador de la página se basa en la variable $choix pasada por el programa valeurs.php. Cabe señalar aquí que la estructura HTML de la página está comenzando a verse seriamente «contaminada» por código PHP. El front-end valeurs.php podría realizar más trabajo, como lo muestra la siguiente versión:

<?php

     // configuración
  ini_set("register_globals","off");

    // la tabla de valores
  $valeurs=array("un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix");

   // se recupera la selección del usuario, si la hay
  $choix=$_POST["cmbValeurs"];

   // se calcula la lista de valores a mostrar
  $HTMLvaleurs="";
    for($i=0;$i<count($valeurs);$i++){
        // si la opción actual es igual a la selección, se selecciona
        if (isset($choix) && $choix==$valeurs[$i])
            $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
      else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
    }//para

   // se calcula la segunda parte de la página
  $HTMLpart2="";
     if(isset($choix)){
        $HTMLpart2="<hr>\n";
        $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
  }//si

   // se muestra la respuesta  
  include "valeurs-p2.php";
?>

La página ahora se genera mediante el siguiente programa valeurs-p2.php:

<html>
    <head>
      <title>Génération de formulaire</title>
  </head>
  <body>
      <h2>Choisissez un nombre</h2>
    <hr>
    <form name="frmvaleurs" method="post" action="valeurs.php">
        <select name="cmbValeurs" size="1">
          <?php
             // visualización de la lista de valores
          echo $HTMLvaleurs;
        ?>
      </select>
           <input type="submit" value="Envoyer" name="cmdEnvoyer">
       </form>
    <?php
         // se muestra la parte 2
      echo $HTMLpart2;
    ?>
  </body>
</html>

El código HTML ya no contiene gran parte del código PHP. Sin embargo, recordemos el objetivo de la división en un programa front-end que analiza y procesa la solicitud de un cliente y programas que simplemente se encargan de mostrar páginas configuradas con los datos transmitidos por el front-end: se trata de separar el trabajo del desarrollador PHP del del diseñador gráfico. El desarrollador PHP trabaja en la interfaz, mientras que el diseñador gráfico se encarga de las páginas web. En nuestra nueva versión, el diseñador gráfico ya no puede, por ejemplo, trabajar en la parte 2 de la página, ya que ya no tiene acceso al código HTML de la misma. En la primera versión, sí podía hacerlo. Por lo tanto, ninguno de los dos métodos es perfecto.

3.9.3. Generación dinámica de formularios - 3

Retomamos el mismo problema que antes, pero esta vez los valores se obtienen de una base de datos. En nuestro ejemplo, se trata de la base MySQL:

  • la base se llama dbValeurs
  • su propietario es admDbValeurs, con la contraseña mdpDbValeurs
  • la base de datos tiene una única tabla llamada tvaleurs
  • esta tabla solo tiene un campo entero llamado valor

dos> mysql --database=dbValeurs --user=admDbValeurs --password=mdpDbValeurs

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

mysql> describe tvaleurs;
+--------+---------+------+-----+---------+-------+
| Field  | Type    | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+-------+
| valeur | int(11) |      |     | 0       |       |
+--------+---------+------+-----+---------+-------+

mysql> select * from tvaleurs;
+--------+
| valeur |
+--------+
|      0 |
|      1 |
|      2 |
|      3 |
|      4 |
|      6 |
|      5 |
|      7 |
|      8 |
|      9 |
+--------+
10 rows in set (0.00 sec)

En una aplicación que utiliza una base de datos, por lo general se encuentran los siguientes pasos:

  1. Conexión a SGBD
  2. Envío de consultas SQL a una base de datos SGBD
  3. Procesamiento de los resultados de estas consultas
  4. Cierre de la conexión con SGBD

Los pasos 2 y 3 se repiten, y el cierre de la conexión solo se lleva a cabo al finalizar el uso de la base de datos. Se trata de un esquema relativamente común para cualquier persona que haya utilizado una base de datos de manera interactiva. La siguiente tabla presenta las instrucciones PHP para realizar estas diferentes operaciones con SGBD y MySQL:

connexion au SGBD
$connexion = mysql_pconnect($hote, $user, $pwd)
$connexion=mysql_connect($hote,$user,$pwd)
$hote: nombre de Internet de la máquina en la que se ejecutan los archivos SGBD y MySQL. De hecho, es posible trabajar con SGBD y MySQL remotos.
$user: nombre de un usuario conocido de SGBD y MySQL
$pwd: su contraseña
$connexion: la conexión creada
mysql_pconnect establece una conexión persistente con SGBD y MySQL. Dicha conexión no se cierra al final del script. Permanece abierta. Así, cuando sea necesario abrir una nueva conexión con el SGBD, el PHP buscará una conexión existente que pertenezca al mismo usuario. Si la encuentra, la utiliza. Esto supone un ahorro de tiempo.
mysql_connect crea una conexión no persistente que, por lo tanto, se cierra cuando finaliza el trabajo con SGBD y MySQL.
requêtes SQL
$résultats = mysql_db_query ($base, $requête, $connexion)
$base: la base de datos de MySQL con la que vamos a trabajar
$requête: una consulta SQL (insertar, eliminar, actualizar, seleccionar, ...)
$connexion: la conexión a SGBD MySQL
$résultats: los resultados de la consulta; varían según si la consulta es un select o una operación de actualización (insertar, actualizar, eliminar, etc.)
traitement des
 résultats d'un
 select
$résultats=mysql_db_query($base,"select ...",$connexion)
El resultado de una consulta «select» es una tabla, es decir, un conjunto de filas y columnas. Se puede acceder a esta tabla mediante $résultats.
$ligne=mysql_fetch_row($résultats) lee una fila de la tabla y la coloca en $ligne en forma de matriz. Así, $ligne[i] es la columna i de la fila recuperada. La función mysql_fetch_row puede invocarse repetidamente. Cada vez, lee una nueva fila de la tabla $résultats. Cuando se llega al final de la tabla, la función devuelve el valor false. De esta manera, la tabla $résultats se puede utilizar de la siguiente manera:
while($línea = mysql_fetch_row($resultados)) {
// procesa la fila actual $fila
}//while
traitement des 
résultats d'une 
requête de mise à 
jour
$résultats = mysql_db_query($base, «insertar...», $connexion)
El valor $résultats es verdadero o falso, dependiendo de si la operación se realizó con éxito o falló. En caso de éxito, la función mysql_affected_rows permite conocer el número de filas modificadas por la operación de actualización.
fermeture de la
 connexion
mysql_close($conexión)
$connexion: una conexión a SGBD MySQL

El código del front-end valeurs.php queda así:

<?php

     // configuración
  ini_set("register_globals","off");
    ini_set("display_errors","off");
    ini_set("track_errors","on");

    // la tabla de valores
  list($erreur,$valeurs)=getValeurs();

   // ¿Hubo algún error?
  if($erreur){
       // visualización de la página de error
    include "valeurs-err.php";
     // fin
    return;
  }//si

   // se recupera la selección del usuario, si la hay
  $choix=$_POST["cmbValeurs"];

   // se calcula la lista de valores a mostrar
  $HTMLvaleurs="";
    for($i=0;$i<count($valeurs);$i++){
        // si la opción actual es igual a la selección, se selecciona
        if (isset($choix) && $choix==$valeurs[$i])
            $HTMLvaleurs.="<option selected>$valeurs[$i]</option>\n";
      else $HTMLvaleurs.="<option>$valeurs[$i]</option>\n";
    }//for

   // se calcula la segunda parte de la página
  $HTMLpart2="";
     if(isset($choix)){
        $HTMLpart2="<hr>\n";
        $HTMLpart2.="<h3>Vous avez choisi le nombre $choix</h3>\n";
  }//si

   // se muestra la respuesta  
  include "valeurs-p1.php";

   // fin
  return;

  // ------------------------------------------------------------------------
  function getValeurs(){
       // recupera los valores de una base de datos MySQL
    $user="admDbValeurs";
    $pwd="mdpDbValeurs";
    $db="dbValeurs";
    $hote="localhost";
    $table="tvaleurs";
    $champ="valeur";

     // abre una conexión persistente con el servidor MySQL
     // o, de lo contrario, una conexión normal
    ($connexion=mysql_pconnect($hote,$user,$pwd)) 
        || ($connexion=mysql_connect($hote,$user,$pwd));
    if(! $connexion)
        return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

     // obtención de los valores
    $selectValeurs=mysql_db_query($db,"select $champ from $table",$connexion);
    if(! $selectValeurs)
        return array("Base de données indisponible(".mysql_error()."). Veuillez recommencer ultérieurement.");

     // los valores se almacenan en una matriz
    $valeurs=array();
    while($ligne=mysql_fetch_row($selectValeurs)){
        $valeurs[]=$ligne[0];
    }//mientras

     // cierre de la conexión (si es persistente, en realidad no se cerrará)
    mysql_close($connexion);

     // retorno del resultado
    return array("",$valeurs);
  }//getValeurs

?>

En esta ocasión, los valores que se deben ingresar en el menú desplegable no provienen de una tabla, sino de la función getValeurs(). Esta función:

  • abre una conexión persistente (mysql_pconnect) con el servidor mySQL, pasando un nombre de usuario registrado y su contraseña.
  • Una vez obtenida la conexión, se envía una consulta select para recuperar los valores presentes en la tabla tvaleurs de la base de datos dbValeurs.
  • El resultado de select se almacena en la tabla $valeurs, la cual se devuelve al programa que la invocó.
  • De hecho, la función devuelve una matriz con dos resultados ($erreur, $valeurs), donde el primer elemento es un posible mensaje de error o, de lo contrario, la cadena vacía.
  • El programa que realiza la llamada verifica si hubo un error o no y, de ser así, muestra la página valeurs-err.php. Esta es la siguiente:
<html>
    <head>
      <title>Erreur</title>
  </head>
  <body>
      <h3>L'erreur suivante s'est produite</h3>
    <font color="red">
        <h4><?php echo $erreur ?></h4>
    </font>
  </body>
</html>
  • Si no se ha producido ningún error, el programa que realiza la llamada tiene los valores disponibles en la tabla $valeurs. Por lo tanto, volvemos al problema anterior.

A continuación se muestran dos ejemplos de ejecución:

  • con error

Image

  • sin error

Image

3.9.4. Generación dinámica de formularios - 4

En el ejemplo anterior, ¿qué pasaría si cambiáramos SGBD? ¿Si pasáramos de MySQL a Oracle o a SQL Server, por ejemplo? Habría que reescribir la función getValeurs(), que proporciona los valores. La ventaja de haber reunido en una función el código necesario para recuperar los valores que se mostrarán en la lista es que el código que hay que modificar está bien localizado y no disperso por todo el programa. La función getValeurs() se puede reescribir para que sea independiente del SGBD utilizado. Basta con que trabaje con el controlador ODBC de SGBD en lugar de hacerlo directamente con SGBD.

Existen numerosas bases de datos en el mercado. Para estandarizar el acceso a las bases de datos en MS Windows, Microsoft ha desarrollado una interfaz llamada ODBC (Open DataBase Connectivity). Esta capa oculta las particularidades de cada base de datos tras una interfaz estándar. En Windows existen numerosos controladores que facilitan el acceso a las bases de datos. A continuación, por ejemplo, se muestra una lista de controladores instalados en una máquina con Windows:

Image

El SGBD MySQL también cuenta con un controlador ODBC. Una aplicación que utilice controladores ODBC puede acceder a cualquiera de las bases de datos mencionadas anteriormente sin necesidad de reescribir el código.

Hagamos que nuestra base de datos MySQL dbValeurs sea accesible a través de un controlador ODBC. El procedimiento que se describe a continuación corresponde a Windows 2000. Para los sistemas Win9x, el procedimiento es muy similar. Activamos el administrador de recursos ODBC:

Image

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

Image

Seleccionamos el controlador ODBC MySQL, hacemos [Terminer] y luego indicamos las características de la fuente de datos:

Image

Windows DSN name 
el nombre asignado a la fuente de datos ODBC (valores ODBC)
MySQL host 
el nombre de la máquina que aloja SGBD y MySQL, que administra la fuente de datos (localhost)
MySQL database name 
el nombre de la base de datos MySQL, que es la fuente de datos (dbValeurs)
User 
un usuario con derechos de acceso suficientes sobre la base de datos MySQL para administrarla (admDbValeurs)
Password 
su contraseña (mdpDbValeurs)

PHP puede trabajar con los controladores ODBC. La siguiente tabla muestra las funciones que conviene conocer:

connexion au SGBD
$connexion = odbc_pconnect($dsn, $user, $pwd)
$connexion=odbc_connect($dsn,$user,$pwd)
$dsn: nombre DSN (Data Source Name) de la máquina en la que se ejecuta el SGBD
$user: nombre de un usuario conocido del SGBD
$pwd: su contraseña
$connexion: la conexión creada
odbc_pconnect establece una conexión persistente con SGBD. Dicha conexión no se cierra al final del script. Permanece abierta. Así, cuando sea necesario abrir una nueva conexión con el SGBD, el PHP buscará una conexión existente que pertenezca al mismo usuario. Si la encuentra, la utiliza. Esto supone un ahorro de tiempo.
odbc_connect crea una conexión no persistente que, por lo tanto, se cierra cuando finaliza el trabajo con SGBD.
requêtes SQL
$requêtePréparée = odbc_prepare ($connexion, $requête)
$requête: una consulta SQL (insertar, eliminar, actualizar, seleccionar, ...)
$connexion: la conexión a SGBD
Analiza la consulta $requête y prepara su ejecución. La consulta así «preparada» se identifica mediante el resultado $requêtePréparée. Preparar una consulta para su ejecución no es obligatorio, pero mejora el rendimiento, ya que el análisis de la consulta solo se realiza una vez. A continuación, se solicita la ejecución de la consulta preparada. Si se solicita la ejecución de una consulta no preparada de manera repetida, el análisis de esta se realiza cada vez, lo cual es innecesario. Una vez preparada, la consulta se ejecuta mediante
$res=odbc_execute($requêtePréparée)
devuelve verdadero o falso dependiendo de si la ejecución de la consulta tiene éxito o no
traitement des
 résultats d'un
 select
El resultado de una consulta SELECT es una tabla, es decir, un conjunto de filas y columnas. Se puede acceder a esta tabla mediante $requêtePréparée.
$res=odbc_fetch_row($requêtePréparée) lee una fila de la tabla de resultados de select. Devuelve verdadero o falso dependiendo de si la ejecución de la consulta tiene éxito o no. Los elementos de la fila recuperada están disponibles a través de la función odbc_result:
$val=odbc_result($requêtePréparée,i): columna i de la fila que se acaba de leer
$val=odbc_result($requêtePréparée,"nomColonne"): columna nomColonne de la línea que se acaba de leer
La función odbc_fetch_row se puede llamar repetidamente. Cada vez que se llama, lee una nueva fila de la tabla de resultados. Cuando se llega al final de la tabla, la función devuelve el valor false. De esta manera, la tabla de resultados se puede utilizar de la siguiente forma:

while(odbc_fetch_row($résultats)){
    // procesa la línea actual con odbc_result
}//mientras
fermeture de la 
connexion
odbc_close($conexión)
$connexion: una conexión a SGBD MySQL

La función getValeurs(), encargada de recuperar los valores de la base de datos ODBC, es la siguiente:

  // ------------------------------------------------------------------------
  function getValeurs(){
       // recupera los valores de una base de datos MySQL
    $user="admDbValeurs";
    $pwd="mdpDbValeurs";
    $db="dbValeurs";
    $dsn="odbc-valeurs";
    $table="tvaleurs";
    $champ="valeur";

     // abre una conexión persistente con el servidor MySQL
     // o, de lo contrario, una conexión normal
    ($connexion=odbc_pconnect($dsn,$user,$pwd)) 
        || ($connexion=odbc_connect($dsn,$user,$pwd));
    if(! $connexion)
        return array("1 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

     // obtención de los valores
    $selectValeurs=odbc_prepare($connexion,"select $champ from $table");
    if(! odbc_execute($selectValeurs))
        return array("2 - Base de données indisponible(".odbc_error()."). Veuillez recommencer ultérieurement.");

     // los valores se almacenan en una matriz
    $valeurs=array();   
    while(odbc_fetch_row($selectValeurs)){
        $valeurs[]=odbc_result($selectValeurs,$champ);
    }//mientras

     // cierre de la conexión (si es persistente, en realidad no se cerrará)
    odbc_close($connexion);

     // retorno del resultado
    return array("",$valeurs);
  }//getValeurs

?>

Si se ejecuta la nueva aplicación sin activar la base de datos odbc-valeurs, se obtiene el siguiente resultado:

Image

Cabe señalar que el código de error devuelto por el controlador ODBC (odbc_error()=S1000) no es muy claro. Si se habilita la base de datos odbc-valeurs, se obtienen los mismos resultados que antes.

En conclusión, se puede decir que esta solución es adecuada para el mantenimiento de la aplicación. De hecho, si la base de datos debe cambiar, la aplicación no tendrá que modificarse. El administrador del sistema simplemente creará una nueva fuente de datos ODBC para la nueva base. Siempre con el mantenimiento en mente, sería conveniente configurar los parámetros de acceso a la base ($dsn, $user, $pwd) en un archivo separado que la aplicación cargue al iniciarse (include).

3.9.5. Recuperar los valores de un formulario

Ya hemos recuperado en varias ocasiones los valores de un formulario enviados por un cliente web. El siguiente ejemplo muestra un formulario que reúne los componentes HTML más comunes y sirve para recuperar los parámetros enviados por el navegador del cliente. El formulario es el siguiente:

Image

Llega prellenado. Luego, el usuario puede modificarlo:

Image

Si presiona el botón [Envoyer], el servidor le devuelve la lista de valores del formulario:

Image

El formulario es una página estática HTML balises.html:

<html>

  <head>
      <title>balises</title>
    <script language="JavaScript">
        function effacer(){
          alert("Vous avez cliqué sur le bouton Effacer");
      }//borrar
        </script>
  </head>

  <body background="/images/standard.jpg">
    <form method="POST" action="parameters.php">
      <table border="0">
        <tr>
          <td>Etes-vous marié(e)</td>
          <td>
              <input type="radio" value="oui" name="R1">Oui
              <input type="radio" name="R1" value="non" checked>Non
          </td>
        </tr>
        <tr>
          <td>Cases à cocher</td>
          <td>
              <input type="checkbox" name="C1" value="un">1
              <input type="checkbox" name="C2" value="deux" checked>2
              <input type="checkbox" name="C3" value="trois">3
          </td>
        </tr>
        <tr>
          <td>Champ de saisie</td>
          <td>
              <input type="text" name="txtSaisie" size="20" value="qqs mots">
          </td>
        </tr>
        <tr>
          <td>Mot de passe</td>
          <td>
              <input type="password" name="txtMdp" size="20" value="unMotDePasse">
          </td>
        </tr>
        <tr>
          <td>Boîte de saisie</td>
          <td>
               <textarea rows="2" name="areaSaisie" cols="20">
ligne1
ligne2
ligne3
</textarea>
          </td>
        </tr>
        <tr>
          <td>combo</td>
          <td>
              <select size="1" name="cmbValeurs">
                <option>choix1</option>
                <option selected>choix2</option>
                <option>choix3</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>liste à choix simple</td>
          <td>
              <select size="3" name="lst1">
                <option selected>liste1</option>
                <option>liste2</option>
                <option>liste3</option>
                <option>liste4</option>
                <option>liste5</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>liste à choix multiple</td>
          <td>
              <select size="3" name="lst2[]" multiple>
                <option selected>liste1</option>
                <option>liste2</option>
                <option selected>liste3</option>
                <option>liste4</option>
                <option>liste5</option>
              </select>
          </td>
        </tr>
        <tr>
          <td>bouton</td>
          <td>
              <input type="button" value="Effacer" name="cmdEffacer" onclick="effacer()">
          </td>
        </tr>
        <tr>
          <td>envoyer</td>
          <td>
              <input type="submit" value="Envoyer" name="cmdRenvoyer">
          </td>
        </tr>
        <tr>
          <td>rétablir</td>
          <td>
              <input type="reset" value="Rétablir" name="cmdRétablir">
          </td>
        </tr>
      </table>
      <input type="hidden" name="secret" value="uneValeur">
    </form>
  </body>
</html>

La tabla a continuación resume la función de las diferentes etiquetas de este documento y el valor recuperado por PHP para los distintos tipos de componentes de un formulario. El valor de un campo con el nombre HTML C puede enviarse mediante un POST o un GET. En el primer caso, se recuperará en la variable $_GET["C"] y, en el segundo caso, en la variable $_POST["C"]. La siguiente tabla supone el uso de un POST.

Etiqueta de control
etiqueta HTML
valor recuperado por PHP
formulaire
<form method="POST" >
 
champ de 
saisie
<input type="text" name="txtSaisie" size="20" value="unas palabras">
$_POST["txtSaisie"]: valor contenido en el campo txtSaisie del formulario
champ de
 saisie cachée
<input type="password" name="txtMdp" size="20" value="unMotDePasse">
$_POST["txtmdp"]: valor contenido en el campo txtMdp del formulario
champ de 
saisie
multilignes
<textarea rows="2" name="areaSaisie" cols="20">
línea1
línea 2
línea 3
</textarea>
$_POST["areaSaisie"]: líneas contenidas en el campo areaSaisie en forma de una sola cadena de caracteres: "ligne1\r\nligne2\r\nligne3". Las líneas están separadas entre sí por la secuencia «\r\n».
boutons radio
<input type="radio" value="Sí" name="R1">Sí
<input type="radio" name="R1" value="no" checked>No
$_POST["R1"]: valor (=value) del botón de radio marcado como «sí» o «no», según corresponda.
cases à
cocher
<input type="checkbox" name="C1" value="uno">1
<input type="checkbox" name="C2" value="dos" checked>2
<input type="checkbox" name="C3" value="tres">3
$_POST["C1"]: valor (=value) de la casilla si está marcada; de lo contrario, la variable no existe. Así, si se ha marcado la casilla C1, $_POST["C1"] es igual a «uno»; de lo contrario, $_POST["C1"] no existe.
Combo
<select size="1" name="cmbValeurs">
<option>opción1</option>
<option selected>opción2</option>
<option>opción3</option>
</select>
$_POST["cmbValeurs"]: opción seleccionada en la lista, por ejemplo, «opción3».
liste à
sélection
unique
<select size="3" name="lst1">
<option selected>lista1</option>
<option>lista2</option>
<option>lista3</option>
<option>lista4</option>
<option>lista5</option>
</select>
$_POST["lst1"]: opción seleccionada de la lista, por ejemplo, «lista5».
liste à 
sélection
multiple
<select size="3" name="lst2[]" multiple>
<option>lista1</option>
<option>lista2</option>
<option selected>lista3</option>
<option>lista4</option>
<option>lista5</option>
</select>
$_POST["lst2"]: tabla de las opciones seleccionadas en la lista, por ejemplo, ["liste3,"liste5"]. Cabe destacar la sintaxis particular de la etiqueta HTML para este caso específico: lst2[].
champ caché
<input type="hidden" name="secret" value="uneValeur">
$_POST["secret"]: valor (=value) del campo, en este caso «uneValeur».

En nuestro ejemplo, los valores del formulario se envían al programa parameters.php:

    <form method="POST" action="parameters.php">

El código de este último es el siguiente:

<?php

   // configuración
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // método de llamada
  $méthode=$_SERVER["REQUEST_METHOD"];

   // recuperación de parámetros
   // depende del método de envío de los mismos
  if($méthode=="GET")
      $param=$_GET;
      else $param=$_POST;
  $R1=$param["R1"];
  $C1=$param["C1"];
  $C2=$param["C2"];
  $C3=$param["C3"];
  $txtSaisie=$param["txtSaisie"];    
  $txtMdp=$param["txtMdp"];
  $areaSaisie=implode("<br>",explode("\r\n",$param["areaSaisie"]));
  $cmbValeurs=$param["cmbValeurs"];
  $lst1=$param["lst1"];
  $lst2=implode("<br>",$param["lst2"]);
  $secret=$param["secret"];

     // ¿Solicitud válida?
    $requêteValide=isset($R1) && (isset($C1) || isset($C2) || isset($C3))
        && isset($txtSaisie) && isset($txtMdp) && isset($areaSaisie)
        && isset($cmbValeurs) && isset($lst1) && isset($lst2) 
        && isset($secret);

   // visualización de la página
    if ($requêteValide)
      include "parameters-p1.php";
    else include "balises.html";                    
?>

Veamos algunos detalles de este programa:

  • la aplicación no depende del modo de transmisión de los valores del formulario al servidor. En los dos casos posibles (GET y POST), el diccionario de valores transmitidos se referencia mediante $param.
  if($méthode=="GET")
      $param=$_GET;
      else $param=$_POST;
  • A partir del contenido del campo areaSaisie «línea1\r\nlínea2\r\n...», se crea una tabla de cadenas mediante explode("\r\n", $param["areaSaisie"]). Así, obtenemos el array [ligne1,ligne2,...]. A partir de este, se crea la cadena "ligne1<br>ligne2<br>..." con la función implode.
  • El valor de la lista de selección múltiple lst2 es una matriz, por ejemplo, ["option3","option5"]. A partir de esta, se crea una cadena de caracteres «option3<br>option5» con la función implode.
  • La aplicación verifica que se hayan configurado todos los parámetros. Hay que recordar aquí que cualquier URL puede invocarse manualmente o mediante un programa y que no necesariamente se cuentan con los parámetros esperados. Si faltan parámetros, se muestra la página balises.html; de lo contrario, se muestra la página parameters-p1.php. Esta última muestra en una tabla los valores recuperados y calculados en parameters.php:
<html>
    <head>
        <title>Récupération des paramètres d'un formulaire</title>
    </head>
    <body>
        <table border="1">
            <tr>
                <td>R1</td>
                <td><?php echo $R1 ?></td>
            </tr>
            <tr>
                <td>C1</td>
                <td><?php echo $C1 ?></td>
            </tr>
            <tr>
                <td>C2</td>
                <td><?php echo $C2 ?></td>
            </tr>
            <tr>
                <td>C3</td>
                <td><?php echo $C3 ?></td>
            </tr>
            <tr>
                <td>txtSaisie</td>
                <td><?php echo $txtSaisie ?></td>
            </tr>
            <tr>
                <td>txtMdp</td>
                <td><?php echo $txtMdp ?></td>
            </tr>
            <tr>
                <td>areaSaisie</td>
                <td><?php echo $areaSaisie ?></td>
            </tr>
            <tr>
                <td>cmbValeurs</td>
                <td><?php echo $cmbValeurs ?></td>
            </tr>
            <tr>
                <td>lst1</td>
                <td><?php echo $lst1 ?></td>
            </tr>
            <tr>
                <td>lst2</td>
                <td><?php echo $lst2 ?></td>
            </tr>
            <tr>
                <td>secret</td>
                <td><?php echo $secret ?></td>
            </tr>
        </table>
    </body>
    </html>    

3.10. Seguimiento de sesión

3.10.1. El problema

Una aplicación web puede consistir en varios intercambios de formularios entre el servidor y el cliente. En ese caso, el funcionamiento es el siguiente:

paso 1

  1. El cliente C1 abre una conexión con el servidor y realiza su solicitud inicial.
  2. El servidor envía el formulario F1 al cliente C1 y cierra la conexión establecida en el paso 1.

paso 2

  1. El cliente C1 llena el formulario y lo envía de vuelta al servidor. Para ello, el navegador abre una nueva conexión con el servidor.
  2. Este procesa los datos del formulario 1, calcula la información I1 a partir de ellos, envía un formulario F2 al cliente C1 y cierra la conexión abierta en el paso 3.

paso 3

  1. El ciclo de los pasos 3 y 4 se repite en los pasos 5 y 6. Al finalizar el paso 6, el servidor habrá recibido dos formularios, F1 y F2, y a partir de ellos habrá calculado la información I1 y I2.

El problema que se plantea es: ¿cómo hace el servidor para conservar la información I1 y I2 relacionada con el cliente C1? A este problema se le conoce como el seguimiento de la sesión del cliente C1. Para comprender su origen, analicemos el esquema de una aplicación de servidor TCP-IP que atiende simultáneamente a varios clientes:

En una aplicación cliente-servidor TCP-IP clásica:

  • el cliente establece una conexión con el servidor
  • intercambia datos con el servidor a través de ella
  • la conexión es cerrada por uno de los dos participantes

Los dos puntos importantes de este mecanismo son:

  1. se crea una conexión única para cada uno de los clientes
  2. esta conexión se utiliza durante todo el tiempo que dura el diálogo entre el servidor y su cliente

Lo que le permite al servidor saber en un momento dado con qué cliente está trabajando es la conexión o, dicho de otra manera, el «canal» que lo conecta con su cliente. Dado que este canal está dedicado a un cliente específico, todo lo que llega por este canal proviene de ese cliente y todo lo que se envía por este canal llega a ese mismo cliente.

El mecanismo cliente-servidor HTTP sigue el esquema anterior, con la particularidad de que la comunicación cliente-servidor se limita a un único intercambio entre el cliente y el servidor:

  • el cliente abre una conexión con el servidor y realiza su solicitud
  • el servidor responde y cierra la conexión

Si en el momento T1, un cliente C realiza una solicitud al servidor, obtiene una conexión C1 que servirá para el intercambio único de solicitud-respuesta. Si en el momento T2, ese mismo cliente realiza una segunda solicitud al servidor, obtendrá una conexión C2 diferente de la conexión C1. Para el servidor, no hay ninguna diferencia entre esta segunda solicitud del usuario C y su solicitud inicial: en ambos casos, el servidor considera al cliente como un cliente nuevo. Para que exista un vínculo entre las diferentes conexiones del cliente C al servidor, es necesario que el cliente C sea «reconocido» por el servidor como un «usuario habitual» y que el servidor recupere la información que tiene sobre ese usuario habitual.

Imaginemos un sistema de administración que funcionara de la siguiente manera:

  • Hay una sola fila de espera
  • Hay varias ventanillas. Por lo tanto, se puede atender a varios clientes al mismo tiempo. Cuando se libera una ventanilla, un cliente sale de la fila de espera para ser atendido en esa ventanilla
  • Si es la primera vez que el cliente se presenta, la persona en la ventanilla le entrega un boleto con un número. El cliente solo puede hacer una pregunta. Cuando recibe su respuesta, debe retirarse de la ventanilla y pasar al final de la fila de espera. El empleado de la ventanilla anota la información de este cliente en un expediente con su número.
  • Cuando vuelve a ser su turno, el cliente puede ser atendido por un cajero diferente al de la vez anterior. Este le pide su ficha y recupera el expediente con el número de la ficha. De nuevo, el cliente hace una consulta, recibe una respuesta y se agrega información a su expediente.
  • Y así sucesivamente... Con el tiempo, el cliente obtendrá la respuesta a todas sus solicitudes. El seguimiento entre las diferentes solicitudes se realiza gracias al token y al expediente asociado a este.

El mecanismo de seguimiento de sesión en una aplicación web cliente-servidor es similar al funcionamiento anterior:

  • en su primera solicitud, el servidor web le asigna un token al cliente
  • presentará este token en cada una de sus solicitudes posteriores para identificarse

El token puede adoptar diferentes formas:

  • el de un campo oculto en un formulario
    • el cliente realiza su primera solicitud (el servidor lo reconoce porque el cliente no tiene un token)
    • El servidor envía su respuesta (un formulario) e incluye el token en un campo oculto del mismo. En ese momento, se cierra la conexión (el cliente abandona la ventanilla con su token). El servidor se ha encargado, si corresponde, de asociar información a ese token.
    • El cliente realiza su segunda solicitud al reenviar el formulario. El servidor recupera el token de dicho formulario. De esta manera, puede procesar la segunda solicitud del cliente al tener acceso, gracias al token, a la información calculada durante la primera solicitud. Se agrega nueva información al expediente vinculado al token, se envía una segunda respuesta al cliente y la conexión se cierra por segunda vez. El token se ha vuelto a incluir en el formulario de la respuesta para que el usuario pueda presentarlo en su próxima solicitud.
    • Y así sucesivamente...

La principal desventaja de esta técnica es que el token debe incluirse en un formulario. Si la respuesta del servidor no es un formulario, el método del campo oculto ya no es viable.

  • El método de la cookie
    • el cliente realiza su primera solicitud (el servidor lo reconoce porque el cliente no tiene token)
    • el servidor responde agregando una cookie en los encabezados HTTP de la respuesta. Esto se hace mediante el comando HTTP Set-Cookie:

Set-Cookie: param1=valor1;param2=valor2;....

donde parami son los nombres de los parámetros y valeursi sus valores. Entre los parámetros se encontrará el token. Muy a menudo, en la cookie solo se encuentra el token, mientras que el servidor almacena el resto de la información en la carpeta vinculada al token. El navegador que recibe la cookie la almacenará en un archivo en el disco. Tras la respuesta del servidor, se cierra la conexión (el cliente sale del portal con su token).

  • (continuación)
    • el cliente realiza su segunda solicitud al servidor. Cada vez que se realiza una solicitud a un servidor, el navegador revisa entre todas las cookies que tiene para ver si hay alguna proveniente del servidor solicitado. Si es así, la envía al servidor siempre en forma de un comando HTTP, el comando «Cookie», que tiene una sintaxis similar a la del comando Set-Cookie utilizado por el servidor:

Cookie: param1=valor1;param2=valor2;....

Entre los encabezados HTTP enviados por el navegador, el servidor encontrará el token que le permite reconocer al cliente y recuperar la información relacionada con él.

Esta es la forma más utilizada de token. Presenta una desventaja: un usuario puede configurar su navegador para que no acepte cookies. Este tipo de usuario, por lo tanto, no tiene acceso a las aplicaciones web que utilizan cookies.

  • Reescritura de URL
    • el cliente realiza su primera solicitud (el servidor lo reconoce porque el cliente no tiene un token)
    • El servidor envía su respuesta. Esta contiene enlaces que el usuario debe utilizar para continuar con la aplicación. En el URL de cada uno de estos enlaces, el servidor agrega el token en el formato URL;token=valor.
    • Cuando el usuario hace clic en uno de los enlaces para continuar con la aplicación, el navegador realiza su solicitud al servidor web enviándole en los encabezados HTTP el URL URL;token=valor solicitado. De esta manera, el servidor puede recuperar el token.

3.10.2. Los valores API y PHP para el seguimiento de la sesión

A continuación, presentamos los principales métodos útiles para el seguimiento de la sesión:

session_start()
inicia la sesión a la que pertenece la solicitud actual. Si esta aún no formaba parte de una sesión, se crea dicha sesión.
session_id()
identificador de la sesión actual
$_SESSION[$var]
diccionario que almacena los datos de una sesión. Accesible para lectura y escritura
session_destroy()
elimina los datos contenidos en la sesión actual. Estos datos siguen estando disponibles para el intercambio cliente-servidor en curso, pero no estarán disponibles en el próximo intercambio.

3.10.3. Ejemplo 1

Presentamos un ejemplo inspirado en el libro «Programación con J2EE», publicado por Wrox y distribuido por Eyrolles. Este ejemplo permite ver cómo funciona una sesión PHP. La página principal es la siguiente:

Image

En ella se encuentran los siguientes elementos:

  • el ID de la sesión obtenido mediante la función session_id(). Este ID, generado por el navegador, se envía al cliente mediante una cookie que el navegador devuelve cuando solicita un URL de la misma estructura de árbol. Esto es lo que mantiene la sesión.
  • un contador que se incrementa a medida que el navegador realiza solicitudes y que indica que la sesión se mantiene correctamente.
  • un enlace que permite eliminar los datos asociados a la sesión actual. Esto se realiza mediante la función session_destroy()
  • un enlace para recargar la página

El código de la aplicación cycledevie.php es el siguiente:

<?php
     //cycledevie.php

     // configuración
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // se inicia una sesión
  session_start();

   // ¿Hay que invalidarla?
  $action=$_GET["action"];
  if($action=="invalider"){
       // fin de sesión
    session_destroy();
  }//si

     // gestión del contador
  if(! isset($_SESSION["compteur"]))
         // el contador no existe: lo creamos
      $_SESSION["compteur"]=0;
         // el contador existe: se incrementa
    else $_SESSION["compteur"]++;

   // se recupera el ID de la sesión actual
  $idSession=session_id();

   // se recupera el contador
  $compteur=$_SESSION["compteur"];

   // se pasa el control a la página de visualización
  include "cycledevie-p1.php";
?>

Cabe destacar lo siguiente:

  • Desde el inicio de la aplicación, se inicia una sesión. Si el cliente ha enviado un token de sesión, se reinicia la sesión con ese identificador y todos los datos relacionados con ella se almacenan en el diccionario $_SESSION. De lo contrario, se crea un nuevo token de sesión.
  • Si el cliente ha enviado un parámetro action con el valor «invalider», los datos de la sesión se marcan como «para eliminar» para el próximo intercambio. No se guardarán en el servidor al final del intercambio, a diferencia de lo que ocurre en un intercambio normal.
  • Se recupera un contador vinculado a la sesión en el diccionario $_SESSION, así como el ID de la sesión (session_id()).
  • La página que se enviará al cliente es generada por el programa cycledevie-p1.php

La página cycledevie-p1.php muestra la página enviada al cliente:

<html>
    <head>
      <title>Gestion de sessions</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : <?php echo $idSession ?>
    <br>compteur : <?php echo $compteur ?>    
    <br><a href="cycledevie.php?action=invalider">Invalider la session</a>
    <br><a href="cycledevie.php">Recharger la page</a>
  </body>
</html>

Cabe destacar el código URL que aparece en cada uno de los dos enlaces:

  1. cycledevie.php para recargar la página
  2. cycledevie.php?action=invalider para invalidar la sesión. En este caso, se agrega un parámetro action=invalider al URL. El programa del servidor cycledevie.php lo recuperará mediante la instrucción $action=$_GET["action"].

Actualicemos la página dos veces seguidas:

Image

El contador se ha incrementado correctamente. El ID de la sesión no ha cambiado. Ahora invalidemos la sesión:

Image

Se observa que se ha perdido el identificador de sesión ID, pero que el contador se ha incrementado una vez más. Actualicemos la página:

Image

Se observa que se vuelve a comenzar con el mismo ID de sesión que antes. El contador, por su parte, vuelve a cero. Por lo tanto, la función session_destroy() no tiene un efecto inmediato. Los datos de la sesión actual se eliminan únicamente para el intercambio cliente-servidor que sigue al momento en que se realiza la eliminación. La sesión ID no ha cambiado, lo que parecería indicar que session_destroy() no inicia una nueva sesión con la creación de un nuevo ID. La cookie del token de sesión fue reenviada por el navegador del cliente y PHP recuperó la sesión de ese token, sesión que ya no contenía datos.

Las pruebas anteriores se realizaron con el navegador Netscape configurado para utilizar cookies. Ahora lo configuraremos para que no las utilice. Esto significa que no almacenará ni reenviará las cookies enviadas por el servidor. Por lo tanto, es de esperarse que las sesiones ya no funcionen. Probemos con un primer intercambio:

Image

Tenemos un ID de sesión, el generado por session_start(). Recarguemos la página con el enlace:

Image

Sorprendentemente, los resultados anteriores muestran que tenemos una sesión activa y que el contador se maneja correctamente. ¿Cómo es esto posible si, al haber desactivado las cookies, ya no hay intercambio de tokens entre el servidor y el navegador? El URL de la captura de pantalla anterior nos da la respuesta:

http://localhost/poly/sessions/1/cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c

Este es el URL del enlace «Actualizar la página». Revisemos el código fuente de la página que muestra el navegador:

<a href="cycledevie.php?action=invalider&PHPSESSID=587ce26f943a288d8f41212e30fed13c">Invalider la session</a>
<a href="cycledevie.php?PHPSESSID=587ce26f943a288d8f41212e30fed13c">Recharger la page</a>

Recordemos que el código inicial de los dos enlaces en la página cycledevie-p1.php es este:

    <a href="cycledevie.php?action=invalider">Invalider la session</a>
    <a href="cycledevie.php">Recharger la page</a>

Por lo tanto, el intérprete PHP reescribió por sí mismo los URL de los dos enlaces añadiéndoles el token de sesión. De esta manera, el navegador transmite correctamente este token cuando se activan los enlaces. Esto explica por qué, incluso sin las cookies activadas, la sesión se sigue gestionando correctamente.

3.10.4. Ejemplo 3

Nos proponemos escribir una aplicación PHP que sea cliente de la aplicación compteur anterior. La llamaría N veces seguidas, donde N se pasaría como parámetro. Nuestro objetivo es mostrar un cliente web programado y la forma de gestionar el token de sesión. Nuestro punto de partida será un cliente web genérico llamado de la siguiente manera:

clientweb URL GET/HEAD

  • URL: URL solicitada
  • GET/HEAD: GET para solicitar el código HTML de la página, HEAD para limitarse únicamente a los encabezados HTTP

A continuación se muestra un ejemplo con el URL http://localhost/poly/sessions/2/cycledevie.php. Este programa es el mismo que ya se describió, con una ligera diferencia:

   // se establece la ruta de la cookie
    session_set_cookie_params(0,"/poly/sessions/2");
  // se inicia una sesión
  session_start();

La función session_set_cookie_params permite establecer ciertos parámetros de la cookie que contendrá el token de sesión. El primer parámetro es la vida útil de la cookie. Una vida útil nula significa que la cookie es eliminada por el navegador que la recibió al cerrarse. El segundo parámetro es la ruta de URL a la que el navegador debe devolver la cookie. En el ejemplo anterior, si el navegador recibió la cookie del equipo localhost, la reenviará a cualquier URL que se encuentre en el árbol de directorios http://localhost/poly/sessions/2/.

dos>e:\php43\php.exe clientweb.php http://localhost/poly/sessions/2/cycledevie.php GET
HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 13:58:16 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: text/html

<html>
        <head>
        <title>Gestion de sessions</title>
  </head>
  <body>
        <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : 48d5aaa0e99850b17c33a6e22d38e5c4    <br>compteur : 0
    <br><a href="cycledevie.php?action=invalider&PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Invalid
er la session</a>
    <br><a href="cycledevie.php?PHPSESSID=48d5aaa0e99850b17c33a6e22d38e5c4">Recharger la page</a>
  </body>
</html>

El programa clientweb muestra todo lo que recibe del servidor. Arriba se ve el comando HTTP Set-cookie, con el cual el servidor envía una cookie a su cliente. En este caso, la cookie contiene dos datos:

  • PHPSESSID, que es el token de la sesión
  • path, que define la URL a la que pertenece la cookie. path=/poly/sessions/2 le indica al navegador que deberá reenviar la cookie al servidor cada vez que solicite una URL que comience con /poly/sessions/2 de la máquina que le envió la cookie.
  • Una cookie también puede definir un período de validez. En este caso, esa información no está presente. Por lo tanto, la cookie se eliminará al cerrar el navegador. Una cookie puede tener un período de validez de N días, por ejemplo. Mientras sea válida, el navegador la reenviará cada vez que se consulte una de las URL de su dominio (Path). Tomemos como ejemplo un sitio de venta en línea de CD. Este puede seguir el recorrido de su cliente por su catálogo y determinar poco a poco sus preferencias: la música clásica, por ejemplo. Estas preferencias pueden almacenarse en una cookie con una vigencia de 3 meses. Si ese mismo cliente regresa al sitio después de un mes, el navegador enviará la cookie a la aplicación del servidor. Esta, basándose en la información contenida en la cookie, podrá entonces adaptar las páginas generadas a las preferencias de su cliente.

A continuación se muestra el código del cliente web.

<?php

     // configuración
    dl("php_curl.dll");    // biblioteca CURL

     // sintaxis: $0 URL GET
   // se necesitan tres argumentos
  if($argc != 3){
       // mensaje de error
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
    // detener
    exit(1);
  }//si

   // el tercer argumento debe ser GET o HEAD
  $header=strtolower($argv[2]);
  if($header!="get" && $header!="head"){
       // mensaje de error
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
     // detener
    exit(2);
  }//si

   // el primer argumento es un URL
  $URL=strtolower($argv[1]);

  // preparación de la conexión  
  $connexion=curl_init($URL);
   // configuración de la conexión
  curl_setopt($connexion,CURLOPT_HEADER,1);  
  if($header=="head") curl_setopt($connexion,CURLOPT_NOBODY,1);
  // ejecución de la conexión
  curl_exec($connexion);
   // cierre de la conexión
  curl_close($connexion);
   // fin
  exit(0);  
?>

El programa anterior utiliza la biblioteca CURL:

$connexion=curl_init($URL)
inicializa un objeto CURL con el URL al que se debe llegar
curl_setopt ($connexion,option,valeur)
establece el valor de ciertas opciones de la conexión. Mencionemos las dos que se utilizan en el programa:
CURLOPT_HEADER=1: permite obtener los encabezados HTTP enviados por el servidor
CURLOPT_NOBODY=1: permite ignorar el documento enviado por el servidor detrás de los encabezados HTTP
curl_exec($connexion)
establece la conexión con $URL con las opciones solicitadas. Muestra en pantalla todo lo que envía el servidor
curl_close($connexion)
cierra la conexión

El programa anterior es bastante sencillo. Sin embargo, la biblioteca CURL no permite manipular con precisión la respuesta del servidor, por ejemplo, analizarla línea por línea. El siguiente programa hace lo mismo que el anterior, pero con las funciones de red básicas de PHP. Será el punto de partida para escribir un cliente para nuestra aplicación cycledevie.php.

<?php

     // sintaxis: $0 URL GET/HEAD
  // se requieren tres argumentos
  if($argc != 3){
       // mensaje de error
    fputs(STDERR,"Syntaxe : $argv[0] URL GET/HEAD");
    // parada
    exit(1);
  }//si

   // conexión y visualización del resultado
  $résultats=getURL($argv[1],$argv[2]);
  if(isset($résultats->erreur)){
       // error
    echo "L'erreur suivante s'est produite : $résultats->erreur\n";
  }else{
       // visualización de la respuesta del servidor
    echo $résultats->réponse;
  }//si  

   // fin
  exit(0);

  //-----------------------------------------------------------------------
  function getURL($URL,$header){

      // se conecta a $URL
     // crea un GET o un HEAD según el valor del encabezado
     // la respuesta del servidor constituye el resultado de la función

     // analiza el URL
    $url=parse_url($URL);
     //: el protocolo
    if(strtolower($url["scheme"])!="http"){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//si
     // la máquina
    $hote=$url["host"];
    if(! isset($hote)){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//si
     // el puerto
    $port=$url["port"];
    if(! isset($port)) $port=80;
     // la ruta
    $chemin=$url["path"];
     // la solicitud
    if(isset($url["query"])){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//si

     // análisis de $header
    $header=strtoupper($header);
      if($header!="GET" && $header!="HEAD"){
          // mensaje de error
        $résultats->erreur="méthode [$header] doit être GET ou HEAD";
         // parada
        return $résultats;
      }//si

     // apertura de una conexión en el puerto $port de $hote
    $connexion=fsockopen($hote,$port,&$errno,&$erreur);
     // retorno si hay error
    if(! $connexion){
      $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
      return $résultats;
    }//si

     // $connexion representa un flujo de comunicación bidireccional
     // entre el cliente (este programa) y el servidor web al que se contacta
     // este canal se utiliza para el intercambio de comandos e información
     // el protocolo de comunicación es HTTP

     // el cliente envía el comando get para solicitar el URL /
    // sintaxis get URL HTTP/1.0
    // los encabezados (headers) del protocolo HTTP deben terminar con una línea en blanco
    fputs($connexion, "$header $chemin HTTP/1.0\n\n");

     // el servidor responderá ahora en el canal $connexion. Enviará todos
     // estos datos y luego cerrará el canal. Por lo tanto, el cliente lee todo lo que llega desde $connexion
     // hasta que se cierre el canal
    $résultats->réponse="";
    while($ligne=fgets($connexion,10000))
      $résultats->réponse.=$ligne;

     // el cliente cierra la conexión a su vez
    fclose($connexion);
     // volver
    return $résultats;
  }//getURL

?>

Comentemos algunos puntos de este programa:

  • el programa admite dos parámetros:
    • un enlace http de tipo URL cuyo contenido se desea mostrar en pantalla.
    • un método GET o HEAD, que se debe utilizar según si solo se desean las cabeceras HTTP (HEAD) o también el cuerpo del documento asociado al URL (GET).
  • Ambos parámetros se pasan a la función getURL. Esta devuelve un objeto $résultats. Este objeto tiene un campo erreur si hay un error, y un campo réponse en caso contrario. El campo erreur sirve para almacenar un posible mensaje de error. El campo réponse es la respuesta del servidor web contactado.
  • La función getURL analiza el URL y el $URL con la función parse_url. La instrucción $url=parse_url($URL) creará el tablero asociativo $url con las siguientes claves, si las hay:
    • scheme: el protocolo de URL (http, ftp, ...)
    • host: la máquina del URL
    • port: el puerto de URL
    • path: la ruta de URL
    • cadena de consulta: los parámetros de URL

Una URL será correcta si tiene el formato http://machine[:port][/chemin].

  • También se verifica el parámetro $header
  • Una vez que los parámetros se han verificado y son correctos, se crea una conexión TCP en la máquina ($hote, $port), luego se envía el comando HTTP, GET o HEAD, según el parámetro $header.
  • luego se lee la respuesta del servidor y se almacena en $résultats->respuesta.

La ejecución del programa arroja los siguientes resultados:

dos>"e:\php43\php.exe" geturl.php http://localhost/poly/sessions/2/cycledevie.php get

HTTP/1.1 200 OK
Date: Wed, 09 Oct 2002 14:56:55 GMT
Server: Apache/1.3.24 (Win32)
Set-Cookie: PHPSESSID=ea0d2673811ed069e7289d86933a4c0a; path=/poly/sessions/2
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html

<html>
        <head>
        <title>Gestion de sessions</title>
  </head>
  <body>
        <h3>Cycle de vie d'une session PHP</h3>
    <hr>
    <br>ID session : ea0d2673811ed069e7289d86933a4c0a    <br>compteur : 0
    <br><a href="cycledevie.php?action=invalider&PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Invalid
er la session</a>
    <br><a href="cycledevie.php?PHPSESSID=ea0d2673811ed069e7289d86933a4c0a">Recharger la page</a>
  </body>
</html>

El lector atento habrá notado que la respuesta del servidor varía según el programa cliente utilizado. En el primer caso, el servidor envió un encabezado HTTP: Transfer-Encoding: chunked, encabezado que no se envió en el segundo caso. Esto se debe a que el segundo cliente envió el encabezado HTTP: get URL HTTP/1.0, que solicita un URL e indica que opera con el protocolo HTTP versión 1.0, lo que obliga al servidor a responderle con ese mismo protocolo. Sin embargo, el encabezado HTTP Transfer-Encoding: chunked pertenece al protocolo HTTP versión 1.1. Por lo tanto, el servidor no lo utilizó en su respuesta. Esto nos muestra que el primer cliente realizó su solicitud indicando que trabajaba con el protocolo HTTP versión 1.1.

Ahora creamos el programa clientCompteur, que se llama de la siguiente manera:

clientCompteur URL N [JSESSIONID]

  • URL: URL de la aplicación cycledevie
  • N: número de llamadas que se realizarán a esta aplicación
  • PHPSESSID: parámetro opcional —token de sesión

El objetivo del programa es llamar N veces a la aplicación cycledevie.php, gestionando la cookie de sesión y mostrando cada vez el valor del contador devuelto por el servidor. Al final de las N llamadas, el valor de este debe ser N-1. A continuación se muestra un primer ejemplo de ejecución:

dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 3
--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 0]

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 1]

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:27:48 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 2]

El programa muestra:

  • los encabezados HTTP que envía al servidor en el formato --> entêteEnvoyé
  • los encabezados HTTP que recibe en el formato <-- entêteReçu
  • el valor del contador después de cada llamada

Se observa que durante la primera llamada:

  • el cliente no envía ninguna cookie
  • el servidor envía una (Set-Cookie:)

Para las llamadas siguientes:

  • el cliente devuelve sistemáticamente la cookie que recibió del servidor en la primera llamada. Esto es lo que le permitirá al servidor reconocerlo e incrementar su contador.
  • El servidor, por su parte, ya no reenvía ninguna cookie

Volvemos a ejecutar el programa anterior pasando el token anterior como tercer parámetro:

dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 1 2425e00d1d65c2bdcbafc1ce6244f7ea

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
--> Cookie: PHPSESSID=2425e00d1d65c2bdcbafc1ce6244f7ea
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 06:32:03 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 3]

Aquí vemos que, desde la primera solicitud del cliente, el servidor recibe una cookie de sesión válida. Esto podría indicar una posible brecha de seguridad. Si puedo interceptar un token de sesión en la red, entonces puedo hacerme pasar por la persona que inició la sesión. En nuestro ejemplo, la primera llamada (sin token de sesión) representa a quien inicia la sesión (quizás con un nombre de usuario y una contraseña que le otorgan el derecho a obtener un token) y la segunda llamada (con token de sesión) representa a quien ha «hackeado» el token de sesión de la primera llamada. Si la operación en curso es una transacción bancaria, esto puede volverse problemático...

El código del cliente es el siguiente:

<?php

     // sintaxis: $0 URL N [PHPSESSID]
   // se necesitan tres argumentos
  if($argc!=3 && $argc!=4){
       // mensaje de error
    fputs(STDERR,"Syntaxe : $argv[0] URL N [PHPSESSID]");
    // detener
    exit(1);
  }//si

   // recuperación de parámetros
  $URL=$argv[1];
  $N=$argv[2];
  $PHPSESSID=$argv[3];

  // inicio de sesión y visualización del resultado
  $résultats=getURL($URL,$N,$PHPSESSID);
  if(isset($résultats->erreur)){
       // error
    echo "L'erreur suivante s'est produite : $résultats->erreur\n";
  } 

   // fin
  exit(0);

  //-----------------------------------------------------------------------
  function getURL($URL,$N,$PHPSESSID){

       // se conecta a URL
     // genera un GET o un HEAD según el valor del encabezado
     // la respuesta del servidor forma el resultado de la función

     //analiza el URL
    $url=parse_url($URL);
     //: el protocolo
    if(strtolower($url["scheme"])!="http"){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//si
     // la máquina
    $hote=$url["host"];
    if(! isset($hote)){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//si
     // el puerto
    $port=$url["port"];
    if(! isset($port)) $port=80;
     // la ruta
    $chemin=$url["path"];
     // la consulta
    if(isset($url["query"])){
        $résultats->erreur="l'URL [$URL] n'est pas au format http://machine[:port][/chemin]";
      return $résultats;
    }//si

         // verificación de $N
    if (! preg_match("/^\d+$/",$N)){
        // error
      $résultats->erreur="nombre [$N] erroné";
       // fin
      return $résultats;
    }//si

     // se realizan las llamadas de $N a $URL
    for($i=0;$i<$N;$i++){
      // apertura de una conexión en el puerto $port de $hote
      $connexion=fsockopen($hote,$port,&$errno,&$erreur);
       // retorno en caso de error
      if(! $connexion){
        $résultats->erreur="Echec de la connexion au site ($hote,$port) : $erreur";
        return $résultats;
      }//si

       // $connexion representa un flujo de comunicación bidireccional
       // entre el cliente (este programa) y el servidor web contactado
       // este canal se utiliza para el intercambio de comandos e información
       // el protocolo de comunicación es HTTP

       // el cliente envía los encabezados HTTP
      // get URL HTTP/1.1
      envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: host:puerto
      envoie($connexion, "Host: $hote:$port\n");
       // Conexión: cerrar
      envoie($connexion, "Connection: close\n");
       // Cookie: $PHPSESSID
      if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
       // línea vacía
      envoie($connexion,"\n");      

       // el servidor responderá ahora en el canal $connexion. Enviará todos
       // sus datos y luego cerrará el canal.
       // El cliente comienza leyendo los encabezados HTTP que terminan con una línea en blanco
      $CHUNKED=0;
      while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
           // eco de línea
        echo "<-- $ligne\n";
         // búsqueda del token si aún no se ha encontrado
        if(! $PHPSESSID){
             // búsqueda de la línea «set-cookie»
          if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
              // se ha encontrado el token; se almacena
            $PHPSESSID=$champs[1];            
          }//si
        }//si
        // búsqueda del modo de transferencia del documento
        if(! $CHUNKED){
             // búsqueda de la línea Transfer-Encoding: chunked
          if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
              // transferencia por fragmentos
            $CHUNKED=1;                        
          }//si
        }//si
      }//línea siguiente

       // eco de línea
      echo "<-- $ligne\n";

       // la lectura del documento depende de cómo se haya enviado
      if($CHUNKED) $document=getChunkedDoc($connexion);
          else $document=getDoc($connexion);

       // búsqueda del contador en el documento
      if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
           // se encontró el contador; se muestra
        echo "\n[Le compteur est égal à $champs[1]]\n\n";
      }//si

       // el cliente cierra la conexión
      fclose($connexion);
    }//para i
    }//getURL

  //--------------------------
  function getDoc($connexion){
      // lectura del documento en $connexion
    $doc="";
    while($ligne=fread($connexion,10000))
        $doc.=$ligne;
     // fin
    return $doc;
  }//getDoc

  //--------------------------
  function getChunkedDoc($connexion){
       // lectura del documento en $connexion
     // este documento se envía por partes en el formato
     // número de caracteres del fragmento en hexadecimal
     // continuación del fragmento
         // línea en blanco

     // se lee el tamaño del fragmento en la primera línea
    $taille=hexdec(rtrim(fgets($connexion,10000)));    
     // se lee el documento siguiente
    $doc="";
    while($taille!=0){
         // lectura de un fragmento de $taille caracteres
      $doc.=fread($connexion,$taille);
       // línea en blanco
      fgets($connexion,10000);
       // fragmento siguiente
       // se lee el tamaño del fragmento
      $taille=hexdec(rtrim(fgets($connexion,10000)));      
    }//mientras
     // se acabó
    return $doc;
  }// getChunkedDoc  

  //--------------------------
  function envoie($flux,$msg){
       // envía $msg a $flux
    fwrite($flux,$msg);
     // imprimir en pantalla
    echo "--> $msg";
  }//envía
?>

Analicemos los puntos importantes de este programa:

  • debemos realizar N intercambios entre el cliente y el servidor. Por eso están dentro de un bucle
            for($i=0;$i<$N;$i++){
  • En cada intercambio, el cliente abre una conexión TCP-IP con el servidor. Una vez establecida la conexión, envía al servidor los encabezados HTTP de su solicitud:
<?php
...
       // el cliente envía los encabezados HTTP
      // get URL HTTP/1.1
      envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: host:puerto
      envoie($connexion, "Host: $hote:$port\n");
       // Conexión: cerrar
      envoie($connexion, "Connection: close\n");
       // Cookie: $PHPSESSID
      if($PHPSESSID) envoie($connexion, "Cookie: PHPSESSID=$PHPSESSID\n");
       // línea en blanco
      envoie($connexion,"\n");      

Si el token PHPSESSID está disponible, se envía en forma de cookie; de lo contrario, no se envía. Cabe señalar que el cliente indicó que trabajaba con el protocolo HTTP/1.1. Esto explica por qué, posteriormente, el servidor le enviará el encabezado HTTP: Transfer-Encoding: chunked, que pertenece al protocolo HTTP/1.1, pero no al protocolo HTTP/1.0.

  • Una vez enviada su solicitud, el cliente espera la respuesta del servidor. Comienza por analizar los encabezados HTTP de dicha respuesta. Busca en ella dos líneas:
Cookie : 
Transfer-Encoding: chunked

La línea «Cookie:» es el encabezado HTTP que contiene el token de sesión PHPSESSID. El cliente debe recuperarlo para reenviarlo al servidor en el siguiente intercambio. La línea «Transfer-Encoding: chunked», si está presente, indica que el servidor enviará un documento por partes. Cada parte se envía entonces al cliente en el siguiente formato:

[nombre de caractères du document en hexadécimal]
[document]
[ligne vide]

Si no aparece la línea Transfer-Encoding: chunked, el documento se envía de una sola vez después de la línea vacía de los encabezados HTTP. Por lo tanto, dependiendo de si esta línea está presente o no, el modo de recepción del documento variará. El código de interpretación de los encabezados HTTP es el siguiente:

<?php
...
       // El cliente comienza leyendo los encabezados HTTP que terminan con una línea en blanco
      $CHUNKED=0;
      while(($ligne=fgets($connexion,10000)) && (($ligne=rtrim($ligne))!="")){
           // eco de línea
        echo "<-- $ligne\n";
         // búsqueda del token si aún no se ha encontrado
        if(! $PHPSESSID){
             // búsqueda de la línea «set-cookie»
          if(preg_match("/^Set-Cookie: PHPSESSID=(.*?);/i",$ligne,$champs)){
              // se ha encontrado el token; se almacena
            $PHPSESSID=$champs[1];            
          }//si
        }//si
        // búsqueda del modo de transferencia del documento
        if(! $CHUNKED){
             // búsqueda de la línea Transfer-Encoding: chunked
          if(preg_match("/^Transfer-Encoding: chunked/i",$ligne,$champs)){
              // transferencia por fragmentos
            $CHUNKED=1;                        
          }//si
        }//si
      }//línea siguiente
  • Una vez que se haya encontrado el token por primera vez, ya no se buscará en las siguientes llamadas al servidor. Una vez procesados los encabezados HTTP de la respuesta, se pasa al documento que sigue a los encabezados HTTP. Este se lee de manera diferente según su modo de transferencia:
<?php
...

       // la lectura del documento depende de cómo se haya enviado
      if($CHUNKED) $document=getChunkedDoc($connexion);
          else $document=getDoc($connexion);
  • En el documento $document recibido, se busca la línea que contiene el valor del contador. Esta búsqueda también se realiza con una expresión regular:
<?php
...
       // búsqueda del contador en el documento
      if(preg_match("/<br>compteur : (\d+)/i",$document,$champs)){
           // se encontró el contador; se muestra
        echo "\n[Le compteur est égal à $champs[1]]\n\n";
      }//si
  • En caso de que el servidor envíe el documento de una sola vez, su recepción es sencilla:
<?php
...

  //--------------------------
  function getDoc($connexion){
       // lectura del documento en $connexion
    $doc="";
    while($ligne=fgets($connexion,10000))
        $doc.=$ligne;
     // fin
    return $doc;
  }//getDoc
  • En caso de que el servidor envíe el documento en varias partes, su lectura resulta más compleja:
<?php
...
  function getChunkedDoc($connexion){
       // lectura del documento en $connexion
     // este documento se envía por partes en el formato
     // número de caracteres del fragmento en hexadecimal
     // continuación del fragmento

     // se lee el tamaño del fragmento en la primera línea
    $taille=hexdec(rtrim(fgets($connexion,10000)));    
     // se lee el documento que sigue
    $doc="";
    while($taille!=0){
         // lectura de un fragmento de $taille caracteres
      $doc.=fread($connexion,$taille);
       // línea en blanco
      fgets($connexion,10000);
       // fragmento siguiente
       // se lee el tamaño del fragmento
      $taille=hexdec(rtrim(fgets($connexion,10000)));      
    }//mientras
     // se acabó
    return $doc;
  }// getChunkedDoc   

Recordemos que un fragmento de documento se envía en el formato

[taille]
[partie de document]
[ligne vide]

Por lo tanto, primero se lee el tamaño del documento. Una vez conocido este, se le pide a la función fread que lea $taille caracteres del flujo $connexion y luego la línea vacía que le sigue. Repetimos esto hasta que el servidor envíe la información de que va a enviar un documento de tamaño 0.

3.10.5. Ejemplo 4

En el ejemplo anterior, el cliente web devuelve el token en forma de cookie. Hemos visto que también puede devolverlo dentro de la misma solicitud URL en el formato URL;PHPSESSID=xxx. Verifiquémoslo. El programa clientCompteur.php se transforma en clientCompteur2.php y se modifica de la siguiente manera:

<?php
...
....
       // el cliente envía los encabezados HTTP
      // get URL HTTP/1.1
      if($PHPSESSID)
          envoie($connexion, "GET $chemin?PHPSESSID=$PHPSESSID HTTP/1.1\n");
        else envoie($connexion, "GET $chemin HTTP/1.1\n");
       // host: host:puerto
      envoie($connexion, "Host: $hote:$port\n");
       // Conexión: cerrar
      envoie($connexion, "Connection: close\n");
       // línea en blanco
      envoie($connexion,"\n");      
....

Por lo tanto, el cliente solicita el URL del medidor mediante GET URL;PHPSESSID=xx HTTP/1.1 y ya no envía una cookie. Este es el único cambio. Estos son los resultados de una primera llamada:


dos>"e:\php43\php.exe" clientCompteur2.php http://localhost/poly/sessions/2/cycledevie.php 2

--> GET /poly/sessions/2/cycledevie.php HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Set-Cookie: PHPSESSID=573212ba82303d7903caf8944ee7a86f; path=/poly/sessions/2
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 0]

--> GET /poly/sessions/2/cycledevie.php?PHPSESSID=573212ba82303d7903caf8944ee7a86f HTTP/1.1
--> Host: localhost:80
--> Connection: close
-->
<-- HTTP/1.1 200 OK
<-- Date: Thu, 10 Oct 2002 07:21:19 GMT
<-- Server: Apache/1.3.24 (Win32)
<-- Expires: Thu, 19 Nov 1981 08:52:00 GMT
<-- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
<-- Pragma: no-cache
<-- Connection: close
<-- Transfer-Encoding: chunked
<-- Content-Type: text/html
<--

[Le compteur est égal à 1]

En la primera solicitud, el cliente solicita el URL sin token de sesión. El servidor le responde enviándole el token. A continuación, el cliente vuelve a solicitar el mismo URL adjuntando el token recibido a la solicitud. Se observa que el contador se incrementa correctamente, lo que demuestra que el servidor reconoció que se trataba de la misma sesión.

3.10.6. Ejemplo 5

Este ejemplo muestra una aplicación compuesta por tres páginas que llamaremos page1, page2 y page3. El usuario debe acceder a ellas en este orden:

  • la página1 es un formulario que solicita un dato: un nombre
  • la página2 es un formulario que se obtiene como respuesta al envío del formulario de la página1. Solicita una segunda información: una edad
  • la página 3 es un documento HTML que muestra el nombre obtenido en la página 1 y la edad obtenida en la página 2.

Hay tres intercambios entre el cliente y el servidor:

  • en el primer intercambio, el cliente solicita el formulario de la página 1 y el servidor lo envía
  • En el segundo intercambio, el cliente envía el formulario de la página1 (nombre) al servidor. A cambio, recibe el formulario de la página2 o, nuevamente, el formulario de la página1 si este contenía un error.
  • En el tercer intercambio, el cliente envía el formulario de la página 2 (edad) al servidor. A cambio, recibe el formulario de la página 3 o, nuevamente, el formulario de la página 2 si este contenía errores. El documento de la página 3 muestra el nombre y la edad. El nombre fue obtenido por el servidor en el segundo intercambio y se ha «olvidado» desde entonces. Se utiliza una sesión para guardar el nombre en el intercambio 2, de modo que esté disponible durante el intercambio 3.

La página «page1» obtenida en el primer intercambio es la siguiente:

Image

Se completa el campo del nombre:

Image

Se utiliza el botón [Suite] y se obtiene entonces la siguiente página page2:

Image

Se completa el campo de la edad:

Image

Se utiliza el botón [Suite] y se obtiene la siguiente página: página3

Image

Al enviar la página «page1» al servidor, este puede devolverla con un código de error si el nombre está vacío:

Image

Al enviar la página page2 al servidor, este puede devolverla con un código de error si la edad no es válida:

Image

La aplicación está compuesta por seis programas:

etape1.php
llama a page1.php
page1.php
muestra la página1. El formulario de la página1 es procesado por etape2.php.
etape2.php
procesa los valores del formulario de la página 1. Si hay errores, page1.php vuelve a mostrar la página 1; de lo contrario, page2.php muestra la página 2.
page2.php
muestra la página2. El formulario de la página2 es procesado por etape3.php.
etape3.php
procesa los valores del formulario de la página 2. Si hay errores, page2.php vuelve a mostrar la página 2; de lo contrario, page3.php muestra la página 3.
page3.php
muestra la página3.

El paso 1 de la aplicación es procesado por el siguiente programa etape1.php:

<?php
   // etape1.php

   // configuración
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // inicio de sesión
  session_start();
  $_SESSION["session"]="";    //reinicio de la variable de sesión

   // preparación de la página 1
    $requête->nom="";
  $requête->erreurs=array();
   // visualización de la página 1
    include "page1.php";
   // fin  
  exit(0);
?>    

Cabe destacar los siguientes puntos:

  • la aplicación requiere un seguimiento de la sesión. Por lo tanto, cada etapa de la misma inicia una sesión.
  • La información de la sesión que se debe conservar se almacenará en un objeto $session.
  • La información necesaria para mostrar las diferentes (tres) páginas de la aplicación se colocará en un objeto $requête.

El programa page1.php muestra la información contenida en $requête:

<? // page1.php ?>

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/3</h3>
    <form name="frmNom" method="POST" action="etape2.php">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<? echo $requête->nom ?>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <? // ¿errores?
      if (count($requête->erreurs)!=0){
    ?>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <? for($i=0;$i<count($requête->erreurs);$i++){ ?>
            <li><? echo $requête->erreurs[$i] ?>
        <? }//para ?>
        </ul>
     <? }//si ?>
  </body>
</html>
  • La página recibe un objeto $requête que contiene dos campos: nom y erreurs. Muestra el valor de estos dos campos.
  • Además, presenta un formulario. Los valores de este (nombre) se envían mediante el método POST al programa etape2.php:
    <form name="frmNom" method="POST" action="etape2.php">

La aplicación etape2.php se encarga de procesar los valores del formulario de la página 1 y de volver a mostrar la página 1 si hay errores (nombre incorrecto); de lo contrario, muestra la página 2 para obtener la edad.

<?php
   // etape2.php

   // configuración
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // inicio de sesión
  session_start();

   // normalmente, debe haber un parámetro «nombre»
   // registrado en la solicitud
  $requête->nom=$_POST["nom"];

   // si no hay parámetros, se envía la página 1 sin errores
  if (! isset($requête->nom)){
      $requête->nom="";
    $requête->erreurs=array();
    include "page1.php";
    exit(0);
  }//si

  // si el parámetro «nombre» está presente, se verifica su validez
  $page=calculerPage($requête);

   // ¿Hubo errores?
  if(count($page->erreurs)!=0){
     // página 1 con errores
    $requête->erreurs=$page->erreurs;
    include "page1.php";
    exit(0);
  }//si

  // no hay errores: se guarda el nombre en la sesión
  unset($session);
  $session->nom=$requête->nom;
  $_SESSION["session"]=$session;

   // visualización de la página 2
  $requête->age="";
  $requête->erreurs=array();
  include "page2.php";

   // fin
  exit(0);

   // ---------calculerPage
  function calculerPage($requête){
     // verifica la validez de la solicitud $requête
     // devuelve una tabla de errores en $page->errores

     // al inicio, no hay errores
    $page->erreurs=array();
     // el nombre no debe estar vacío
    if (preg_match("/^\s*$/",$requête->nom)){
      $page->erreurs[]="Vous n'avez pas indiqué de nom";
    }
     // regresar a la página
    return $page;
  }//calculerPage
?>
  • etape2 comienza por verificar que cuente con el parámetro esperado nom. Si no es así, vuelve a mostrar una página1 vacía. Este caso es posible si etape2 es llamado directamente por un cliente que no le pasa ningún parámetro.
  • Si el parámetro nom está presente, se verifica su validez. Esto se realiza mediante un procedimiento llamado calculerPage, cuya función es generar un objeto $page con un campo erreurs que es una matriz de errores. Solo es posible un error, pero se quiso demostrar que se podía manejar una lista de errores.
  • Si hay errores, se vuelve a mostrar la página page1 junto con la lista de errores.
  • Si no hay errores, el nombre se almacena en el objeto $session, que guarda los datos relacionados con la sesión actual. A continuación, se muestra la página page2.

El programa page2.php muestra la página 2:

<? // page2.php ?>

<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/3</h3>
    <form name="frmAge" method="POST" action="etape3.php">
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><? echo $requête->nom ?></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<? echo $requête->age ?>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <? // ¿Errores?
      if (count($requête->erreurs)!=0){
    ?>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <? for($i=0;$i<count($requête->erreurs);$i++){
            echo "<li>".$requête->erreurs[$i];
           }//para 
        ?>
        </ul>
      </font>
     <? } ?>
  </body>
</html>

El funcionamiento de esta página es muy similar al de page2.php. Muestra el contenido de un objeto $requête que contiene los campos nom, age y erreurs. Muestra un formulario cuyos valores serán procesados por etape3.php.

    <form name="frmAge" method="POST" action="etape3.php">

El programa etape3.php procesa, por lo tanto, los valores del formulario de la página 2, que aquí se reducen a la edad:

<?php
   // etape3.php

   // configuración
  ini_set("register_globals","off");
  ini_set("display_errors","off");

  // inicio de sesión
  session_start();

   // se recuperan los parámetros nombre y edad
  $requête->age=$_POST["age"];
  $session=$_SESSION["session"];
  $requête->nom=$session->nom;

   // Normalmente, debe haber un nombre y una edad
  if (! isset($requête->age) || ! isset($requête->nom)){
     // si la llamada es incorrecta, se envía la página 1
    $_SESSION["session"]="";    // por precaución
    $requête->nom="";
    $requête->erreurs=array();
    include "page1.php";
    exit(0);
  }//si

  // el parámetro «edad» está presente, se verifica su validez
  $page=calculerPage($requête);

   // ¿Hubo algún error?
  if(count($page->erreurs)!=0){
     // página 2 con errores
    $requête->erreurs=$page->erreurs;
    include "page2.php";
    exit(0);
  }//si

  // sin error - almacenamiento de la edad en la sesión
  $session->age=$requête->age;
  $_SESSION["session"]=$session;

   // visualización de la página 3
  include "page3.php";
   // fin
  exit(0);

   // ---------calculerPage
  function calculerPage($requête){
     // verifica la validez de la solicitud $requête
     // devuelve una tabla de errores en $page->errores

     // al inicio, no hay errores
    $page->erreurs=array();
     // la edad debe tener un formato válido
    if (! preg_match("/^\s*\d{1,3}\s*$/",$requête->age)){
      $page->erreurs[]="âge incorrect";
    }
     // regresar a la página
    return $page;
  }//calculerPage
?>
  • El programa comienza recuperando el nombre de la sesión (procedente de la página 1) y la edad del formulario de la página 2. Si falta alguno de estos datos, se muestra la página 1.
  • A continuación, se verifica la validez de la edad. Si la edad es incorrecta, se vuelve a mostrar la página 2 con una lista de errores. Si la edad es correcta, se muestra la página 3. Esta simplemente muestra los dos valores (nombre, edad) obtenidos de los dos formularios (página 1, página 2).
<? // page3.php ?>

<html>
  <head>
    <title>page 3</title>
  </head>
  <body>
    <h3>Page 3/3</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><? echo $requête->nom ?></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><? echo $requête->age ?></font></td>
        </tr>
      </table>
  </body>
</html>