Skip to content

3. Um controlador genérico

3.1. Introdução

No método anterior, ficou entendido que tínhamos de escrever o controlador denominado main.php. Com um pouco de experiência, percebemos que este controlador faz frequentemente as mesmas coisas e, por isso, é tentador escrever um controlador genérico que possa ser utilizado na maioria das aplicações web. O código para este controlador poderia ser o seguinte:

<?php
     // generic controller

   // configurable reading
  include 'config.php';

   // including libraries
  for($i=0;$i<count($dConfig['includes']);$i++){
      include($dConfig['includes'][$i]);
  }//for  

   // start or resume session
  session_start();
  $dSession=$_SESSION["session"];
  if($dSession) $dSession=unserialize($dSession);

   // retrieve the action to be taken
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';
  $sAction=strtolower($_SERVER['REQUEST_METHOD']).":$sAction";

     // is the sequence of actions normal?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
     // abnormal sequence
    $sAction='enchainementinvalide';
  }//if

     // share processing
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;

   // send response(view) to customer
  $sEtat=$dSession['etat']['principal'];
  $scriptVue=$dConfig['etats'][$sEtat]['vue'];
  include $scriptVue;

   // end of script - we shouldn't get there unless there's a 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: configuration dictionary
       // $dSession: dictionary containing session info
         // $dReponse: the dictionary of arguments for the response page

     // session registration
    if(isset($dSession)){
      // put the query parameters in the session
      $dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
          strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
        $_SESSION['session']=serialize($dSession);
      session_write_close();
    }else{    
         // no session
      session_destroy();
    }

         // we present the answer
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

     // end of script
    exit(0);
  }//endsession      

  //--------------------------------------------------------------------
    function enchainementOK(&$dConfig,&$dSession,$sAction){
       // checks whether the current action is authorized with respect to the previous state
    $etat=$dSession['etat']['principal'];
    if(! isset($etat)) $etat='sansetat';

     // check action
    $actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
    $autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
        return $autorise;    
  }

  //--------------------------------------------------------------------
  function dump($dInfos){
       // displays an information dictionary
    while(list($clé,$valeur)=each($dInfos)){
        echo "[$clé,$valeur]<br>\n";
    }//while
  }//follow-up

  //--------------------------------------------------------------------
  function trace($msg){
      echo $msg."<br>\n";
  }//follow-up

?>

3.2. O ficheiro de configuração da aplicação

A aplicação é configurada num script que deve ser denominado config.php. As definições da aplicação são armazenadas num dicionário denominado $dConfig, que é utilizado pelo controlador, pelos scripts de ação, pelos modelos e pelas vistas básicas.

3.3. Bibliotecas a incluir no controlador

As bibliotecas a incluir no código do controlador são colocadas na matriz $dConfig['includes']. O controlador inclui-as com o seguinte fragmento de código:

<?php
...
   // configurable reading
  include "config.php";

   // including libraries
  for($i=0;$i<count($dConfig['includes']);$i++){
      include($dConfig['includes'][$i]);
  }//for  

3.4. Gestão de sessões

O controlador genérico gere automaticamente uma sessão. Ele guarda e recupera o conteúdo da sessão através do dicionário $dSession. Este dicionário pode conter objetos que devem ser serializados para poderem ser recuperados corretamente mais tarde. A chave associada a este dicionário é «session». Portanto, a recuperação de uma sessão é feita com o seguinte código:

<?php

   // start or resume session
  session_start();
  $dSession=$_SESSION["session"];
  if($dSession) $dSession=unserialize($dSession);

Se uma ação precisar de armazenar informações na sessão, irá adicionar chaves e valores ao dicionário $dSession. Uma vez que todas as ações partilham a mesma sessão, existe o risco de conflitos de chaves de sessão se a aplicação for desenvolvida de forma independente por várias pessoas. Isto constitui um desafio. Precisamos de desenvolver um repositório que liste as chaves de sessão, um repositório partilhado por todos. Veremos que cada ação termina com uma chamada à seguinte função finSession:

<?php
... 
 // ---------------------------------------------------------------
  function finSession(&$dConfig,&$dReponse,&$dSession){
     // $dConfig: configuration dictionary
       // $dSession: dictionary containing session information
         // $dReponse: the dictionary of arguments for the response page

     // session registration
    if(isset($dSession)){
      // put the query parameters in the session
      $dSession['requete']=strtolower($_SERVER['REQUEST_METHOD'])=='get' ? $_GET :
          strtolower($_SERVER['REQUEST_METHOD'])=='post' ? $_POST : array();
        $_SESSION['session']=serialize($dSession);
      session_write_close();
    }else{    
         // no session
      session_destroy();
    }

         // we present the answer
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

     // end of script
    exit(0);
  }//endsession      

Uma ação pode decidir não continuar uma sessão. Para tal, basta não passar nenhum valor para o parâmetro $dSession da função endSession, caso em que a sessão é destruída (session_destroy). Se o dicionário $dSession existir, é guardado na sessão, que é então gravada (session_write_close). A ação atual pode, portanto, armazenar elementos na sessão adicionando-os ao dicionário $dSession. Note que o controlador armazena automaticamente os parâmetros da solicitação atual na sessão. Isso permite que sejam recuperados, se necessário, para processar a próxima solicitação.

3.5. Envio da resposta ao cliente

O objetivo final da função finSession é enviar uma resposta ao utilizador. Mencionámos que uma resposta pode ter diferentes modelos de página. Estes são configurados em $dConfig['vuesResponse']. Numa aplicação 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 atual especifica o modelo pretendido em $dResponse['responseView']. O controlador apresenta isto utilizando a seguinte instrução:

<?php

         // we present the answer
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

Assim que esta resposta é enviada ao cliente, o controlador pára (sai).

3.6. Execução de ações

O controlador aguarda pedidos com um parâmetro action=XX. Se este parâmetro não existir no pedido e o pedido for um GET, a ação assume o valor 'init'. É o que acontece no primeiro pedido feito ao controlador, que tem o formato http://machine:port/chemin/main.php.

<?php
..
   // retrieve the action to be taken
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';

Por predefinição, cada ação está associada a um script responsável por processar essa ação. Por exemplo:

<?php
... 
  // configuration of application actions
  $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:

invalidSequence
casos em que a ação atual não pode seguir a ação anterior
ação inválida
casos em que a ação solicitada não existe no dicionário de ações

As ações específicas da aplicação são escritas na forma método:ação, em que método é o método GET ou POST do pedido e ação é a ação solicitada, neste caso: init, calculateTax, returnForm, clearForm. Note-se que a ação é recuperada, independentemente de os parâmetros serem enviados através do método GET ou POST, utilizando a sequência:

<?php

   // retrieve the action to be taken
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init'; 

Na verdade, mesmo que um formulário seja enviado via POST, ainda podemos escrever:

<form method='post' action='main.php?action=calculerimpot'>
..
</form>

Os elementos do formulário serão enviados (method='post'). No entanto, o URL solicitado será main.php?action=calculerimpot. Os parâmetros deste URL serão recuperados da matriz $_GET, enquanto os outros elementos do formulário serão recuperados da matriz $_POST.

Utilizando o dicionário de ações, o controlador executa a ação solicitada da seguinte forma:

<?php
...
    // share processing
  $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 é executado. Note que tem acesso às variáveis do controlador ($dConfig, $dSession), bem como aos dicionários superglobais do PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). O script contém a lógica da aplicação e chamadas às classes de negócio. Em todos os casos, a ação deve

  • preencher o dicionário $dSession se algum elemento precisar de ser guardado na sessão atual
  • especificar em $dResponse['vuereponse'] o nome do modelo de resposta a apresentar
  • terminar com uma chamada a `finSession($dConfig, $dResponse, $dSession)`. Se a sessão for destruída, a ação terminará simplesmente com uma chamada a `finSession($dConfig, $dResponse)`.

Por uma questão de consistência, a ação pode colocar todas as informações necessárias às visualizações no dicionário $dReponse. Mas isso não é obrigatório. Apenas o valor $dReponse['vuereponse'] é essencial. Note-se que todos os scripts de ação terminam com uma chamada à função finSession, que por sua vez termina com uma operação de saída. Portanto, não há retorno de um script de ação.

3.7. A sequência de ações

Uma aplicação web pode ser vista como uma máquina de estados finitos. Os vários estados da aplicação estão associados às vistas apresentadas ao utilizador. O utilizador pode navegar para outra vista através de um link ou de um botão. A aplicação web mudou de estado. Vimos que uma ação é iniciada por um pedido do tipo http://machine:port/chemin/main.php?action=XX. Este URL deve provir de um link contido na vista apresentada ao utilizador. Queremos impedir que um utilizador digite diretamente a URL http://machine:port/chemin/main.php?action=XX, contornando assim o caminho que a aplicação planeou para ele. Isto também se aplica se o cliente for um programa.

Um caminho de navegação é válido se a URL solicitada for uma que possa ser alcançada a partir da última vista apresentada ao utilizador. A lista dessas URLs é fácil de determinar. Consiste nas

  • as URLs contidas na vista, quer como links, quer como destinos para ações do tipo «submeter»
  • URLs que um utilizador está autorizado a digitar diretamente no seu navegador quando a vista é exibida.

A lista de estados da aplicação não é necessariamente a mesma que a lista de vistas. Considere, por exemplo, a seguinte vista simples, **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";
        }//for
    ?>
</ul>
<div class="info"><?php echo $dReponse["info"] ?></div>
<a href="<?php echo $dReponse["href"] ?>"><?php echo $dReponse["lien"] ?></a>

Esta vista básica será integrada numa composição de vistas básicas que formarão a resposta. Nesta vista, existe um link que pode ser posicionado dinamicamente. A vista errors.php pode então ser apresentada com n links diferentes, dependendo das circunstâncias. Isto resultará em n estados diferentes para a aplicação. No estado #i, a vista errors.php será apresentada com o link lieni. Neste estado, apenas é permitido o uso de lieni.

A lista de estados de uma aplicação e as ações possíveis em cada estado serão armazenadas no dicionário $dConfig['etats']:

<?php
...  
// application status configuration
  $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'));

A aplicação acima tem dois estados nomeados: e-form e e-errors. Adicionamos um estado chamado stateless, que corresponde ao arranque inicial da aplicação quando esta não tinha estado. Num estado E, a lista de ações permitidas encontra-se na matriz $dConfig['states'][E]['allowedActions']. Esta especifica o método permitido (get/post) para a ação e o nome da ação. No exemplo acima, existem quatro ações possíveis: get:init, post:calculateTax, get:returnForm e post:clearForm.

Utilizando o dicionário $dConfig['etats'], o controlador pode determinar se a ação atual $sAction é permitida no estado atual da aplicação. Este estado é construído por cada ação e armazenado na sessão em $dSession['etat']. O código do controlador para verificar se a ação atual é permitida é o seguinte:

<?php
.....
     // is the sequence of actions normal?
  if( ! enchainementOK($dConfig,$dSession,$sAction)){  
     // abnormal sequence
    $sAction='enchainementinvalide';
  }//if

     // share processing
  $scriptAction=$dConfig['actions'][$sAction] ? 
    $dConfig['actions'][$sAction]['url'] : 
    $dConfig['actions']['actionInvalide']['url'];
  include $scriptAction;
..........
  //--------------------------------------------------------------------
    function enchainementOK(&$dConfig,&$dSession,$sAction){
       // checks whether the current action is authorized with respect to the previous state
    $etat=$dSession['etat']['principal'];
    if(! isset($etat)) $etat='sansetat';

     // check action
    $actionsautorisees=$dConfig['etats'][$etat]['actionsautorisees'];
    $autorise= ! isset($actionsautorisees) || in_array($sAction,$actionsautorisees);
        return $autorise;    
  }

A lógica é a seguinte: uma ação $sAction é permitida se estiver na lista $dConfig['states'][$state]['allowedActions'], ou se essa lista não existir, caso em que qualquer ação é permitida. $state é o estado da aplicação no final do ciclo anterior de pedido do cliente/resposta do servidor. Este estado foi armazenado na sessão e é recuperado a partir daí. Se a ação solicitada for considerada inválida, o script $dConfig['actions']['invalidSequence']['url'] é executado. Este script encarregar-se-á de enviar uma resposta adequada ao cliente.

Durante a fase de desenvolvimento, o dicionário $dConfig['etats'] pode ser deixado vazio. Neste caso, qualquer estado permite qualquer ação. O dicionário pode ser finalizado assim que a aplicação tiver sido totalmente depurada. Irá 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 exibe uma mensagem na saída HTML
  • a função dump exibe o conteúdo de um dicionário no mesmo fluxo

Qualquer script de ação pode utilizar estas duas funções. Uma vez que o código do script de ação está incluído no código do controlador, as funções trace e dump estarão visíveis para os scripts.

3.9. Conclusão

O controlador genérico foi concebido para permitir que o programador se concentre nas ações e nas vistas da sua aplicação. Ele trata do seguinte por eles:

  • gestão de sessão (restauração, gravação)
  • validação das ações solicitadas
  • execução do script associado à ação
  • envio ao cliente de uma resposta adequada ao resultado da execução da ação