3. Um controlador genérico
3.1. Introdução
No método anterior, ficou entendido que deveríamos escrever o controlador chamado main.php. Com um pouco de experiência, percebe-se que esse controlador costuma realizar as mesmas tarefas, e fica então tentador escrever um controlador genérico que possa ser utilizado na maioria das aplicações web. O código desse controlador poderia ser o seguinte:
<?php
// controlador genérico
// leitura da configuração
include 'config.php';
// inclusão de bibliotecas
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
// iniciamos ou retomamos a sessão
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
// recuperando a ação a ser realizada
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
$sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";
// a sequência de ações está normal?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// sequência anormal
$sAction='enchainementinvalide';
}//if
// processamento da ação
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
// envio da resposta (visualização) ao cliente
$sEtat=$dSession['etat']['principal'];
$scriptVue=$dConfig['etats'][$sEtat]['vue'];
include $scriptVue;
// fim do script — não deveríamos chegar até aqui, a menos que haja um bug
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: dicionário de configuração
// $dSession: dicionário contendo as informações da sessão
// $dReponse: o dicionário de argumentos da página de resposta
// registro da sessão
if(isset($dSession)){
//: os parâmetros da solicitação são inseridos na sessão
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// sem sessão
session_destroy();
}
// apresenta-se a resposta
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// fim do script
exit(0);
}//fim da sessão
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// verifica se a ação atual está autorizada em relação ao estado anterior
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// verificação da ação
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
//--------------------------------------------------------------------
function dump($dInfos){
// exibe um dicionário de informações
while(list($clé,$valeur)=each($dInfos)){
echo "[$clé,$valeur]<br>\n";
}//while
}//acompanhamento
//--------------------------------------------------------------------
function trace($msg){
echo $msg."<br>\n";
}//acompanhamento
?>
3.2. O arquivo de configuração do aplicativo
A aplicação é configurada em um script que deve obrigatoriamente ter o nome config.php. Os parâmetros da aplicação são definidos em um dicionário chamado $dConfig, utilizado tanto pelo controlador quanto pelos scripts de ação, modelos e visualizações elementares.
3.3. As bibliotecas a serem incluídas no controlador
As bibliotecas a serem incluídas no código do controlador estão listadas na tabela $dConfig['includes']. O controlador as inclui com a seguinte sequência de código:
<?php
...
// leitura da configuração
include "config.php";
// inclusão de bibliotecas
for($i=0;$i<count($dConfig['includes']);$i++){
include($dConfig['includes'][$i]);
}//for
3.4. Gerenciamento de sessões
O controlador genérico gerencia automaticamente uma sessão. Ele salva e recupera o conteúdo de uma sessão por meio do dicionário $dSession. Esse dicionário pode conter objetos que precisam ser serializados para que possam ser recuperados corretamente posteriormente. A chave associada a esse dicionário é 'session'. Portanto, a recuperação de uma sessão é feita com o seguinte código:
<?php
…
// inicia ou retoma a sessão
session_start();
$dSession=$_SESSION["session"];
if($dSession) $dSession=unserialize($dSession);
Se uma ação precisar armazenar informações na sessão, ela adicionará chaves e valores ao dicionário $dSession. Como todas as ações compartilham a mesma sessão, há o risco de conflito de chaves de sessão caso o aplicativo seja desenvolvido de forma independente por várias pessoas. Isso representa uma dificuldade. É necessário desenvolver um repositório que liste as chaves de sessão, repositório compartilhado por todos. Veremos que cada ação termina com a chamada à seguinte função finSession:
<?php
...
// ---------------------------------------------------------------
function finSession(&$dConfig,&$dReponse,&$dSession){
// $dConfig: dicionário de configuração
// $dSession: dicionário contendo as informações da sessão
// $dReponse: o dicionário de argumentos da página de resposta
// registro da sessão
if(isset($dSession)){
//: os parâmetros da solicitação são inseridos na sessão
$dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
$_SESSION['session']=serialize($dSession);
session_write_close();
}else{
// sem sessão
session_destroy();
}
// apresenta-se a resposta
include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];
// fim do script
exit(0);
}//fim da sessão
Uma ação pode decidir não dar continuidade a uma sessão. Para isso, basta não passar nenhum valor para o parâmetro $dSession da função finSession; nesse caso, a sessão é excluída (session_destroy). Se o dicionário $dSession existir, ele é salvo na sessão, que é então registrada (session_write_close). A ação em andamento pode, portanto, armazenar elementos na sessão adicionando-os ao dicionário $dSession. Observe que o controlador armazena automaticamente os parâmetros da solicitação atual na sessão. Isso permitirá recuperá-los, se necessário, para processar a próxima solicitação.
3.5. O envio da resposta ao cliente
A função finSession tem como objetivo final enviar uma resposta ao usuário. Já mencionamos que uma resposta pode ter diferentes modelos de página. Esses modelos são definidos por configuração em $dConfig['vuesReponse']. Em um aplicativo com dois modelos, poderíamos ter:
<?php
…
$dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
$dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');
A ação em andamento especifica, em $dReponse['vuereponse'], o modelo desejado. Este é exibido pelo controlador por meio da instrução:
Assim que essa resposta for enviada ao cliente, o controlador encerra a execução (exit).
3.6. A execução das ações
O controlador aguarda solicitações com o parâmetro action=XX. Se esse parâmetro não existir na solicitação e esta for do tipo GET, a ação assume o valor 'init'. Esse é o caso da primeira solicitação enviada ao controlador, que tem o formato http://machine:port/chemin/main.php.
<?php
…..
// recuperando a ação a ser realizada
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
Por padrão, a cada ação está associado um script responsável por processá-la. Por exemplo:
<?php
...
// configuração das ações do aplicativo
$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');
Duas ações estão predefinidas:
caso em que a ação atual não possa seguir a ação anterior | |
caso em que a ação solicitada não exista no dicionário de ações |
As ações específicas do aplicativo são indicadas na forma método:ação, em que método é o método GET ou POST da solicitação e ação é a ação solicitada, neste caso: init, calcularimposto, retornarformulário, apagarformulário. Observe-se que a ação é recuperada, independentemente do método de envio de parâmetros (GET ou POST), pela sequência:
<?php
…
// recuperação da ação a ser realizada
$sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
De fato, mesmo que um formulário seja enviado, ainda é possível escrever:
Os elementos do formulário serão enviados (method='post'). No entanto, a URL solicitada será main.php?action=calculerimpot. Os parâmetros desse URL serão recuperados no dicionário $_GET, enquanto os demais elementos do formulário serão recuperados no dicionário $_POST.
Com o dicionário de ações em mãos, o controlador executa a ação solicitada da seguinte maneira:
<?php
...
// processamento da ação
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
Se a ação solicitada não estiver no dicionário de ações, será executado o script correspondente a uma ação inválida. Assim que o script da ação for carregado no controlador, ele será executado. Observe-se que ele tem acesso às variáveis do controlador ($dConfig, $dSession), bem como aos dicionários superglobais de PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). No script, encontram-se lógica de aplicação e chamadas a classes de negócio. Em todos os casos, a ação deverá
- preencher o dicionário $dSession caso haja elementos a serem salvos na sessão atual
- indicar em $dReponse['vuereponse'] o nome do modelo de resposta a ser exibido
- terminar com a chamada a finSession($dConfig, $dReponse, $dSession). Se a sessão precisar ser encerrada, a ação terminará simplesmente com a chamada para finSession($dConfig, $dReponse).
Por uma questão de coerência, a ação poderá inserir no dicionário $dReponse todas as informações necessárias para as visualizações. Mas isso não é obrigatório. Apenas o valor $dReponse['vuereponse'] é indispensável. Observe-se que todo script de ação termina com a chamada à função finSession, que, por sua vez, termina com uma operação exit. Portanto, não há retorno de um script de ação.
3.7. A sequência de ações
É possível considerar um aplicativo web como um autômato de estados finitos. Os diferentes estados do aplicativo estão associados às visualizações apresentadas ao usuário. Por meio de um link ou de um botão, o usuário passa para outra visualização. O aplicativo web mudou de estado. Vimos que uma ação é iniciada por uma solicitação do tipo http://machine:port/chemin/main.php?action=XX. Esse URL deve provir de um link contido na visualização apresentada ao usuário. De fato, queremos evitar que um usuário digite diretamente o URL http://machine:port/chemin/main.php?action=XX, contornando assim o caminho que o aplicativo previu para ele. Isso também se aplica caso o cliente seja um programa.
Uma sequência será válida se a URL solicitada for uma URL que possa ser solicitada a partir da última visualização apresentada ao usuário. A lista dessas visualizações é fácil de determinar. Ela é constituída
- dos URL contidos na visualização, seja na forma de links, seja na forma de destinos de ações do tipo “submit”
- dos URL que um usuário está autorizado a digitar diretamente em seu navegador quando a visualização é exibida.
A lista de estados do aplicativo não coincide necessariamente com a lista de visualizações. Consideremos, por exemplo, a seguinte visualização elementar 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>
Essa visualização elementar será integrada a uma composição de visualizações elementares que formará a resposta. Nessa visualização, há um link que pode ser posicionado dinamicamente. A visualização erreurs.php pode, então, ser exibida com n links diferentes, dependendo das circunstâncias. Isso dará origem a n estados diferentes para o aplicativo. No estado nº i, a visualização erreurs.php será exibida com o link lieni. Nesse estado, apenas o uso do link lieni é aceitável.
A lista de estados de uma aplicação e das ações possíveis em cada estado será registrada no dicionário $dConfig['etats']:
<?php
...
// configuração dos estados do aplicativo
$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'));
O aplicativo acima possui dois estados denominados: e-formulário e e-erros. Adicionamos um estado chamado semestado, que corresponde ao início inicial do aplicativo, quando ele ainda não possuía nenhum estado. Em um estado E, a lista de ações permitidas encontra-se na tabela $dConfig['etats'][E]['actionsautorisees']. Nela são especificados o método (GET/POST) autorizado para a ação e o nome da mesma. No exemplo acima, há quatro ações possíveis: get:init, post:alculerimpot, get:retourformulaire e post:effacerformulaire.
Com o dicionário $dConfig['etats'], o controlador pode determinar se a ação $sAction em andamento é autorizada ou não no estado atual do aplicativo. Esse estado é construído a cada ação e armazenado na sessão em $dSession['etat']. O código do controlador para verificar se a ação atual é permitida ou não é o seguinte:
<?php
.....
// a sequência de ações está normal?
if( ! enchainementOK($dConfig,$dSession,$sAction)){
// sequência anormal
$sAction='enchainementinvalide';
}//if
// processamento da ação
$scriptAction=$dConfig['actions'][$sAction] ?
$dConfig['actions'][$sAction]['url'] :
$dConfig['actions']['actionInvalide']['url'];
include $scriptAction;
..........
//--------------------------------------------------------------------
function enchainementOK(&$dConfig,&$dSession,$sAction){
// verifica se a ação atual está autorizada em relação ao estado anterior
$etat=$dSession['etat']['principal'];
if(! isset($etat)) $etat='sansetat';
// verificação da ação
$actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
$autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
return $autorise;
}
A lógica é a seguinte: uma ação $sAction é autorizada se estiver na lista $dConfig['etats'][$etat]['actionsautorisees'] ou se essa lista não existir , o que autoriza, então, qualquer ação. $etat é o estado do aplicativo ao final do ciclo anterior de solicitação do cliente/resposta do servidor. Esse estado foi armazenado na sessão e é recuperado a partir dela. Se for constatado que a ação solicitada é inválida, executa-se o script $dConfig['actions']['enchainementInvalide']['url']. Esse script se encarregará de enviar uma resposta adequada ao cliente.
Na fase de desenvolvimento, pode-se optar por não preencher o dicionário $dConfig['etats']. Nesse caso, qualquer estado autoriza qualquer ação. O dicionário poderá ser ajustado quando a aplicação tiver sido totalmente depurada. Ele protegerá a aplicação contra ações não autorizadas.
3.8. Depuração
O controlador oferece duas funções de depuração:
- a função trace permite exibir uma mensagem no fluxo HTML
- a função dump permite exibir o conteúdo de um dicionário nesse mesmo fluxo
Qualquer script de ação poderá utilizar essas duas funções. De fato, como o código do script de ação está incluído (include) no código do controlador, as funções trace e dump estarão visíveis nos scripts.
3.9. Conclusão
O controlador genérico tem como objetivo permitir que o desenvolvedor se concentre nas ações e nas visualizações de sua aplicação. Ele garante para ele:
- o gerenciamento da sessão (restauração, salvamento)
- a verificação da validade das ações solicitadas
- a execução do script associado à ação
- o envio ao cliente da resposta adequada ao resultado da execução da ação