Skip to content

3. Um controlador genérico

3.1. Introdução

No método anterior, ficou entendido que teríamos de escrever o controlador denominado main.php. Com alguma experiência, percebe-se que este controlador realiza frequentemente as mesmas tarefas, pelo que é tentador escrever um controlador genérico utilizável na maioria das aplicações web. O código deste 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);

  // recuperar a ação a realizar
  $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 se deveria chegar 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 que contém as informações da sessão
         // $dReponse: o dicionário dos argumentos da página de resposta

    // registo da sessão
    if(isset($dSession)){
      //: os parâmetros da solicitação são colocados 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 ficheiro de configuração da aplicação

A aplicação é configurada num script que deve obrigatoriamente ter o nome config.php. Os parâmetros da aplicação são definidos num dicionário denominado $dConfig, utilizado tanto pelo controlador como pelos scripts de ação, pelos modelos e pelas vistas elementares.

3.3. As bibliotecas a incluir no controlador

As bibliotecas a incluir no código do controlador estão definidas na tabela $dConfig['includes']. O controlador inclui-as 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. Gestão de sessões

O controlador genérico gere automaticamente uma sessão. Guarda e recupera o conteúdo de uma sessão através do dicionário $dSession. Este dicionário pode conter objetos que têm de ser serializados para poderem ser recuperados corretamente posteriormente. A chave associada a este dicionário é «session». Assim, 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 pretender memorizar informações na sessão, irá adicionar chaves e valores ao dicionário $dSession. Como todas as ações partilham a mesma sessão, existe o risco de conflito de chaves de sessão se a aplicação for desenvolvida de forma independente por várias pessoas. Trata-se de uma dificuldade. É necessário desenvolver um repositório que liste as chaves de sessão, um repositório partilhado 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

    // registo da sessão
    if(isset($dSession)){
      //: os parâmetros da solicitação são colocados 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 tal, basta não atribuir qualquer valor ao parâmetro $dSession da função finSession; nesse caso, a sessão é eliminada (session_destroy). Se o dicionário $dSession existir, é guardado na sessão e esta é, em seguida, registada (session_write_close). A ação em curso pode, portanto, armazenar elementos na sessão, adicionando-os ao dicionário $dSession. Note-se 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 solicitação seguinte.

3.5. O envio da resposta ao cliente

A função finSession tem como objetivo final enviar uma resposta ao utilizador. Já referimos que uma resposta pode ter diferentes modelos de página. Estes são definidos por configuração em $dConfig['vuesReponse']. Numa aplicação com dois modelos, poderíamos assim ter:

<?php

  $dConfig['vuesReponse']['modele1']=array('url'=>'m-modele1.php');
  $dConfig['vuesReponse']['modele2']=array('url'=>'m-modele2.php');

A ação em curso especifica, em $dReponse['vuereponse'], o modelo pretendido. Este é apresentado pelo controlador através da instrução:

<?php

         // apresenta-se a resposta
        include $dConfig['vuesReponse'][$dReponse['vuereponse']]['url'];

Assim que esta resposta for enviada ao cliente, o controlador termina (exit).

3.6. A execução das ações

O controlador aguarda pedidos com o parâmetro action=XX. Se este parâmetro não existir na solicitação e esta for do tipo GET, a ação assume o valor «init». É o caso da primeira solicitação enviada ao controlador, que tem o formato http://machine:port/chemin/main.php.

<?php
..
  // recuperar a ação a realizar
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init';

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

<?php
... 
// configuração das ações da aplicação
  $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');          

Existem duas ações predefinidas:

enchainementInvalide
caso em que a ação em curso não possa seguir a ação anterior
actionInvalide
nos 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 indicadas 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, calcularimposto, devolverformulário, apagarformulário. Note-se que a ação é recuperada, independentemente do método de envio de parâmetros (GET ou POST), através da sequência:

<?php

  // recuperar a ação a realizar
  $sAction=$_GET['action'] ? strtolower($_GET['action']) : 'init'; 

De facto, mesmo que um formulário seja enviado, é sempre possível escrever:

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

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

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

<?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 constar do 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, este é executado. Note-se que tem acesso às variáveis do controlador ($dConfig, $dSession), bem como aos dicionários superglobais de PHP ($_GET, $_POST, $_SERVER, $_ENV, $_SESSION). No script, encontrar-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 seja necessário guardar elementos na sessão atual
  • indicar em $dReponse['vuereponse'] o nome do modelo de resposta a apresentar
  • terminar com a chamada a finSession($dConfig, $dReponse, $dSession). Se a sessão tiver de ser encerrada, a ação terminará simplesmente com a chamada à função 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 vistas. Mas não há qualquer obrigação. Apenas o valor $dReponse['vuereponse'] é indispensável. Note-se que qualquer script de ação termina com a chamada à função finSession, que, por sua vez, termina com uma operação «exit». Por conseguinte, não é possível regressar a partir de um script de ação.

3.7. A sequência de ações

Pode-se considerar uma aplicação web como um autómato de estados finitos. Os diferentes estados da aplicação estão associados às vistas apresentadas ao utilizador. Este, através de um link ou de um botão, passa para outra vista. 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. O objetivo é, de facto, evitar que um utilizador digite diretamente o URL http://machine:port/chemin/main.php?action=XX, contornando assim o percurso que a aplicação previu para ele. Isto também se aplica se o cliente for um programa.

Uma sequência será correta se a URL solicitada for uma URL que possa ser solicitada a partir da última vista apresentada ao utilizador. A lista destas é fácil de determinar. É constituída

  • dos URL contidos na vista, quer sob a forma de links, quer sob a forma de alvos de ações do tipo «submit»
  • dos URL que um utilizador está autorizado a introduzir diretamente no seu navegador quando a vista lhe é apresentada.

A lista de estados da aplicação não coincide necessariamente com a lista de vistas. Consideremos, por exemplo, a seguinte vista 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>

Esta vista elementar será integrada numa composição de vistas elementares que constituirá a resposta. Nesta vista, existe um link que pode ser posicionado dinamicamente. A vista erreurs.php pode, assim, ser apresentada com n links diferentes, consoante as circunstâncias. Isto dará origem a n estados diferentes para a aplicação. No estado n.º i, a vista erreurs.php será apresentada com o link «lieni». Neste estado, apenas é aceitável a utilização do «lieni».

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

<?php
...  
// configuração dos estados da aplicação
  $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 denominados: e-formulário e e-erros. Adiciona-se um estado denominado «sem estado», que corresponde ao arranque inicial da aplicação, quando esta ainda não tinha nenhum estado. Num estado E, a lista de ações autorizadas encontra-se na tabela $dConfig['etats'][E]['actionsautorisees']. Nela é especificado o método (GET/POST) autorizado para a ação e o nome da mesma. No exemplo acima, existem quatro ações possíveis: get:init, post:alculerimpot, get:retourformulaire e post:effacerformulaire.

Com base no dicionário $dConfig['etats'], o controlador pode determinar se a ação $sAction em curso está autorizada ou não no estado atual da aplicação. Este é criado por cada ação e armazenado na sessão em $dSession['etat']. O código do controlador para verificar se a ação atual está autorizada 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 constar da lista $dConfig['etats'][$etat]['actionsautorisees'] ou se essa lista não existir , autorizando assim qualquer ação. $etat é 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 se verificar que a ação solicitada é ilegal, executa-se o script $dConfig['actions']['enchainementInvalide']['url']. Este script encarregar-se-á de transmitir uma resposta adequada ao cliente.

Na fase de desenvolvimento, é possível não preencher o dicionário $dConfig['etats']. Nesse caso, qualquer estado autoriza qualquer ação. O dicionário poderá ser aperfeiçoado quando a aplicação tiver sido totalmente depurada. Este 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 estas duas funções. Com efeito, uma vez que 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 programador se concentre nas ações e nas vistas da sua aplicação. Este assegura-lhe:

  • a gestão da sessão (restauração, gravação)
  • 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 estado resultante da execução da ação