3. Un controlador genérico
3.1. Introducción
En el método anterior, se daba por hecho que teníamos que escribir el controlador llamado main.php. Con un poco de experiencia, nos damos cuenta de que este controlador suele hacer lo mismo y, por lo tanto, resulta tentador escribir un controlador genérico que se pueda utilizar en la mayoría de las aplicaciones web. El código de este controlador podría ser el siguiente:
<?php
// controlador genérico
// lectura de la configuración
include 'config.php';
// Inclusión de bibliotecas
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
// iniciamos o reanudamos la sesión
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
// se recupera la acción a realizar
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
$sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";
// ¿Es normal la secuencia de acciones?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// secuencia anómala
$sAction='enchainementinvalide';
}//si
// procesamiento de la acción
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
// envío de la respuesta (vista) al cliente
$sEtat=$dSession['etat']['principal'];
$scriptVue=$dConfig['etats'][$sEtat]['vue'];
include $scriptVue;
// fin del script: no se debería llegar hasta aquí a menos que haya un error
trace ("Erreur de configuration.");
trace("Action=[$sAction]");
trace("scriptAction=[$scriptAction]");
trace("Etat=[$sEtat]");
trace("scriptVue=[$scriptVue]");
trace ("Vérifiez que les script existent et que le script [$scriptVue] se termine par l'appel à finSession.");
exit(0);
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: diccionario de configuración
// $dSession: diccionario que contiene la información de la sesión
// $dReponse: el diccionario de argumentos de la página de respuesta
//: registro de la sesión
if(isset($dSession)){
//: se guardan los parámetros de la solicitud en la sesión
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// sin sesión
session_destroy();
}
// se muestra la respuesta
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// fin del script
exit(0);
}//fin de sesión
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// verifica si la acción actual está autorizada en relación con el estado anterior
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// verificación de la acción
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
//--------------------------------------------------------------------
function dump($dInfos){
// muestra un diccionario de información
while(list($clé,$valeur)=each($dInfos)){
echo "[$clé,$valeur]<br>\n";
}//mientras
}//seguimiento
//--------------------------------------------------------------------
function trace($msg){
echo $msg."<br>\n";
}//seguimiento
?>
3.2. El archivo de configuración de la aplicación
La aplicación se configura en un script que debe llevar obligatoriamente el nombre config.php. Los parámetros de la aplicación se almacenan en un diccionario llamado $dConfig, que es utilizado tanto por el controlador como por los scripts de acciones, los modelos y las vistas elementales.
3.3. Las bibliotecas que se deben incluir en el controlador
Las bibliotecas que se deben incluir en el código del controlador se encuentran en la tabla $dConfig['includes']. El controlador las incluye con la siguiente secuencia de código:
<?php
...
// lectura de configuración
include "config.php";
// inclusión de bibliotecas
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
3.4. La gestión de sesiones
El controlador genérico administra automáticamente una sesión. Guarda y recupera el contenido de una sesión a través del diccionario $dSession. Este diccionario puede contener objetos que deben serializarse para poder recuperarlos correctamente posteriormente. La clave asociada a este diccionario es «session». Por lo tanto, la recuperación de una sesión se realiza con el siguiente código:
<?php
…
// se inicia o reanuda la sesión
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
Si una acción desea almacenar información en la sesión, agregará claves y valores al diccionario $dSession. Dado que todas las acciones comparten la misma sesión, existe el riesgo de que se produzcan conflictos entre las claves de sesión si la aplicación es desarrollada de manera independiente por varias personas. Esto representa una dificultad. Es necesario desarrollar un repositorio que enumere las claves de sesión, un repositorio compartido por todos. Veremos que cada acción termina con la llamada a la siguiente función finSession:
<?php
...
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: diccionario de configuración
// $dSession: diccionario que contiene la información de la sesión
// $dReponse: el diccionario de argumentos de la página de respuesta
//: registro de la sesión
if(isset($dSession)){
//: se guardan los parámetros de la solicitud en la sesión
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// sin sesión
session_destroy();
}
// se muestra la respuesta
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// fin del script
exit(0);
}//fin de sesión
Una acción puede decidir no continuar con una sesión. Para ello, basta con no pasar ningún valor al parámetro $dSession de la función finSession; en ese caso, la sesión se elimina (session_destroy). Si existe el diccionario $dSession, se guarda en la sesión y esta se registra posteriormente (session_write_close). Por lo tanto, la acción en curso puede almacenar elementos en la sesión al agregar elementos al diccionario $dSession. Cabe señalar que el controlador almacena automáticamente los parámetros de la solicitud actual en la sesión. Esto permitirá recuperarlos si es necesario para procesar la siguiente solicitud.
3.5. El envío de la respuesta al cliente
El objetivo final de la función finSession es enviar una respuesta al usuario. Ya mencionamos que una respuesta puede tener diferentes plantillas de página. Estas se configuran en $dConfig['vuesReponse']. En una aplicación con dos plantillas, podríamos tener, por ejemplo:
<?php
…
$dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
$dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');
La acción en curso especifica en $dReponse['vuereponse'] el modelo deseado. El controlador muestra este modelo mediante la instrucción:
Una vez enviada esta respuesta al cliente, el controlador se detiene (exit).
3.6. La ejecución de las acciones
El controlador espera solicitudes que incluyan el parámetro action=XX. Si este parámetro no existe en la solicitud y esta es de tipo GET, la acción toma el valor «init». Este es el caso de la primera solicitud enviada al controlador, que tiene el formato http://machine:port/chemin/main.php.
<?php
…..
// se recupera la acción a realizar
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
Por configuración, a cada acción se le asigna un script encargado de procesarla. Por ejemplo:
<?php
...
// configuración de las acciones de la aplicación
$dConfig['actions']['get:init']=array('url'=>'a-init.php');
$dConfig['actions']['post:calculerimpot']=array('url'=>'a-calculimpot.php');
$dConfig['actions']['get:retourformulaire']=array('url'=>'a-retourformulaire.php');
$dConfig['actions']['post:effacerformulaire']=array('url'=>'a-init.php');
$dConfig['actions']['enchainementinvalide']=array('url'=>'a-enchainementinvalide.php');
$dConfig['actions']['actionInvalide']=array('url'=>'a-actioninvalide.php');
Hay dos acciones predefinidas:
en caso de que la acción actual no pueda seguir a la acción anterior | |
en los casos en que la acción solicitada no existe en el diccionario de acciones |
Las acciones propias de la aplicación se indican en el formato método:acción, donde método es el método get o post de la solicitud y acción es la acción solicitada, en este caso: init, calcularimpuesto, devolverformulario, borrarformulario. Cabe señalar que la acción se recupera, independientemente del método de envío de parámetros (ya sea GET o POST), mediante la secuencia:
<?php
…
// se recupera la acción a realizar
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
De hecho, aunque se envíe un formulario, siempre se puede escribir:
Los elementos del formulario se enviarán (method='post'). Sin embargo, la URL solicitada será main.php?action=calculerimpot. Los parámetros de este URL se recuperarán del diccionario $_GET, mientras que los demás elementos del formulario se recuperarán del diccionario $_POST.
Con el diccionario de acciones, el controlador ejecuta la acción solicitada de la siguiente manera:
<?php
...
// procesamiento de la acción
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
Si la acción solicitada no se encuentra en el diccionario de acciones, se ejecutará el script correspondiente a una acción no válida. Una vez que el script de la acción se carga en el controlador, se ejecuta. Cabe destacar que tiene acceso a las variables del controlador ($dConfig, $dSession), así como a los diccionarios superglobales de PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). En el script se encontrará lógica de aplicación y llamadas a clases de negocio. En todos los casos, la acción deberá
- rellenar el diccionario $dSession si hay elementos que deben guardarse en la sesión actual
- indicar en $dReponse['vuereponse'] el nombre de la plantilla de respuesta que se debe mostrar
- terminar con la llamada a finSession($dConfig, $dReponse, $dSession). Si se debe eliminar la sesión, la acción simplemente terminará con la llamada a finSession($dConfig, $dReponse).
En aras de la coherencia, la acción podrá incluir en el diccionario $dReponse toda la información necesaria para las vistas. Pero no es obligatorio. Solo es indispensable el valor $dReponse['vuereponse']. Cabe señalar que todo script de acción termina con la llamada a la función finSession, la cual a su vez termina con una operación exit. Por lo tanto, no se regresa de un script de acción.
3.7. La secuencia de acciones
Se puede considerar una aplicación web como un autómata de estados finitos. Los diferentes estados de la aplicación están asociados a las vistas que se le presentan al usuario. Este, mediante un enlace o un botón, pasará a otra vista. La aplicación web ha cambiado de estado. Hemos visto que una acción se inicia mediante una solicitud del tipo http://machine:port/chemin/main.php?action=XX. Este URL debe provenir de un enlace que se encuentra en la vista que se le muestra al usuario. De hecho, queremos evitar que un usuario escriba directamente el código URL http://machine:port/chemin/main.php?action=XX, saltándose así el proceso que la aplicación ha previsto para él. Esto también se aplica si el cliente es un programa.
Una secuencia será correcta si la URL solicitada es una URL a la que se puede acceder desde la última vista mostrada al usuario. La lista de estas es fácil de determinar. Está compuesta
- de los URL contenidos en la vista, ya sea en forma de enlaces o como destinos de acciones de tipo «submit»
- los URL que un usuario puede ingresar directamente en su navegador cuando se le presenta la vista.
La lista de estados de la aplicación no coincide necesariamente con la de las vistas. Consideremos, por ejemplo, la siguiente vista elemental erreurs.php:
Les erreurs suivantes se sont produites :
<ul>
<?php
for($i=0;$i<count($dReponse["erreurs"]);$i++){
echo "<li class='erreur'>".$dReponse["erreurs"][$i]."</li>\n";
}//para
?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dReponse["href"] ?>"><?php echo $dReponse["lien"] ?></a>
Esta vista elemental se integrará en una composición de vistas elementales que formará la respuesta. En esta vista, hay un enlace que se puede posicionar dinámicamente. La vista erreurs.php puede mostrarse con n enlaces diferentes según las circunstancias. Esto dará lugar a n estados diferentes para la aplicación. En el estado n.º i, la vista erreurs.php se mostrará con el enlace enlacei. En este estado, solo es aceptable el uso del enlacei.
La lista de estados de una aplicación y las acciones posibles en cada estado se registrarán en el diccionario $dConfig['etats']:
<?php
...
// configuración de los estados de la aplicación
$dConfig['etats']['e-formulaire']=array(
'actionsautorisees'=>array('post:calculerimpot','get:init','post:effacerformulaire'),
'vue'=>'e-formulaire2.php');
$dConfig['etats']['e-erreurs']=array(
'actionsautorisees'=>array('get:retourformulaire','get:init'),
'vue'=>'e-erreurs2.php');
$dConfig['etats']['sansetat']=array('actionsautorisees'=>array('get:init'));
La aplicación anterior tiene dos estados denominados: e-formulario y e-errores. Se agrega un estado llamado sin estado, que corresponde al inicio inicial de la aplicación cuando aún no tenía ningún estado. En un estado E, la lista de acciones permitidas se encuentra en la tabla $dConfig['etats'][E]['actionsautorisees']. Allí se especifica el método (GET/POST) autorizado para la acción y el nombre de esta. En el ejemplo anterior, hay cuatro acciones posibles: get:init, post:alculerimpot, get:retourformulaire y post:effacerformulaire.
Con el diccionario $dConfig['etats'], el controlador puede determinar si la acción $sAction en curso está autorizada o no en el estado actual de la aplicación. Este se genera con cada acción y se almacena en la sesión en $dSession['etat']. El código del controlador para verificar si la acción actual está autorizada o no es el siguiente:
<?php
.....
// ¿Es normal la secuencia de acciones?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// secuencia anómala
$sAction='enchainementinvalide';
}//si
// procesamiento de la acción
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
..........
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// verifica si la acción actual está autorizada con respecto al estado anterior
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// verificación de la acción
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
La lógica es la siguiente: una acción $sAction está autorizada si se encuentra en la lista $dConfig['etats'][$etat]['actionsautorisees'] o si dicha lista no existe , lo que permite entonces cualquier acción. $etat es el estado de la aplicación al final del ciclo anterior de solicitud del cliente/respuesta del servidor. Este estado se guardó en la sesión y se recupera de ahí. Si se detecta que la acción solicitada es ilegal, se ejecuta el script $dConfig['actions']['enchainementInvalide']['url']. Este script se encargará de enviar una respuesta adecuada al cliente.
Durante la fase de desarrollo, es posible que no se complete el diccionario $dConfig['etats']. En ese caso, cualquier estado autoriza cualquier acción. El diccionario se podrá perfeccionar una vez que la aplicación haya sido completamente depurada. Este protegerá a la aplicación de acciones no autorizadas.
3.8. Depuración
El controlador ofrece dos funciones de depuración:
- la función «trace» permite mostrar un mensaje en el flujo HTML
- la función «dump» permite mostrar el contenido de un diccionario en ese mismo flujo
Cualquier script de acción podrá utilizar estas dos funciones. De hecho, dado que el código del script de acción está incluido (include) en el código del controlador, las funciones «trace» y «dump» serán visibles desde los scripts.
3.9. Conclusión
El controlador genérico tiene como objetivo permitir que el desarrollador se concentre en las acciones y las vistas de su aplicación. Para ello, se encarga de:
- la gestión de la sesión (restauración, guardado)
- la verificación de la validez de las acciones solicitadas
- la ejecución del script asociado a la acción
- el envío al cliente de la respuesta adecuada al estado resultante de la ejecución de la acción