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 denominado 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
// lecture config
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';
}//if
// 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 introducen 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);
}//finsession
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// comprueba 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;
}
//--------------------------------------------------------------------
function dump($dInfos){
// muestra un diccionario de información
while(list($clé,$valeur)=each($dInfos)){
echo "[$clé,$valeur]<br>\n";
}//while
}//suivi
//--------------------------------------------------------------------
function trace($msg){
echo $msg."<br>\n";
}//suivi
?>
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, 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 deben incluirse en el código del controlador se colocan en la tabla $dConfig['includes']. El controlador las incluye con la siguiente secuencia de código:
<?php
...
// lecture config
include "config.php";
// Inclusión de bibliotecas
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
3.4. Gestión de sesiones
El controlador genérico gestiona 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
…
// 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, añadirá 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 forma independiente por varias personas. Esto supone 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 introducen 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);
}//finsession
Una acción puede decidir no continuar una sesión. Para ello, basta con no pasar ningún valor al parámetro $dSession de la función finSession, en cuyo caso se elimina la sesión (session_destroy). Si existe el diccionario $dSession, se guarda en la sesión y esta se registra a continuación (session_write_close). Por lo tanto, la acción en curso puede almacenar elementos en la sesión añadiendo 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. Envío de la respuesta al cliente
El objetivo final de la función finSession es enviar una respuesta al usuario. Hemos dicho 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:
<?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. Este es mostrado por el controlador 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 con 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 realizada 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 asocia un script encargado de procesar dicha acción. 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:
caso en el que la acción en curso no puede seguir a la acción anterior | |
caso en el 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, calculerimpot, retourformulaire, effacerformulaire. Cabe señalar que la acción se recupera, independientemente del método de envío de parámetros (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 en el diccionario $_GET, mientras que los demás elementos del formulario se recuperarán en el diccionario $_POST.
Una vez que dispone del 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 cargado el script de la acción en el controlador, este 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 va a mostrar
- terminar con la llamada a finSession($dConfig, $dReponse, $dSession). Si la sesión debe eliminarse, la acción terminará simplemente 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, que a su vez termina con una operación exit. Por lo tanto, no se vuelve 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 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 proceder de un enlace contenido en la vista que se muestra al usuario. De hecho, queremos evitar que un usuario escriba directamente URL http://machine:port/chemin/main.php?action=XX, saltándose así el recorrido que la aplicación ha previsto para él. Esto también es válido si el cliente es un programa.
Una secuencia será correcta si la URL solicitada es una URL que se puede solicitar desde la última vista mostrada al usuario. La lista de estas es fácil de determinar. Está formada
- de los URL contenidos en la vista, ya sea en forma de enlaces o en forma de destinos de acciones de tipo «submit»
- de los URL que un usuario está autorizado a escribir 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:
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";
}//for
?>
</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 entonces 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 lieni. En este estado, solo es aceptable el uso de lieni.
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 añade un estado llamado sinestado 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']. En ella 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 crea 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';
}//if
// procesamiento de la acción
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
..........
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// comprueba 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 ha almacenado en la sesión y se recupera allí. 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 rellene el diccionario $dConfig['etats']. En ese caso, cualquier estado autoriza cualquier acción. El diccionario se podrá perfeccionar cuando la aplicación haya sido totalmente depurada. Protegerá la aplicación de acciones no autorizadas.
3.8. Depuración
El controlador ofrece dos funciones de depuración:
- la función de rastreo 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 al desarrollador centrarse en las acciones y las vistas de su aplicación. 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