Skip to content

7. Estudo de caso: gestão de uma base de dados de artigos na Internet

Os códigos deste estudo de caso estão disponíveis |ICI|.

Objetivos:

  • escrever uma classe para a gestão de uma base de dados de artigos
  • escrever uma aplicação web baseada nessa classe
  • introduzir as folhas de estilo
  • propor uma abordagem inicial à metodologia de desenvolvimento para aplicações web simples
  • introduzir JavaScript no navegador do cliente

Créditos: A essência deste estudo de caso foi retirada do livro «Les cahiers du programmeur - PHP/MySQL», de Jean-Philippe Leboeuf, publicado pela editora Eyrolles.

7.1. Introdução

Um comerciante pretende gerir os artigos que vende na loja. Já dispõe de uma aplicação ACCESS que realiza essa tarefa, mas sente-se tentado pela aventura da Web. Possui uma conta junto de um fornecedor de acesso à Internet, que permite aos seus clientes instalar scripts PHP nas suas pastas pessoais. Isto permite-lhes criar sites dinâmicos. Além disso, esses mesmos clientes dispõem de uma conta MySQL que lhes permite criar tabelas que podem fornecer dados aos seus scripts PHP. Assim, o comerciante tem uma conta MySQL com o nome de utilizador «adarticles» e a palavra-passe «mdparticles». Possui uma base de dados «dbarticles» sobre a qual tem todos os direitos. O nosso comerciante dispõe, portanto, dos elementos necessários para colocar a sua gestão de artigos na Web. Com a ajuda de si, que possui competências em desenvolvimento Web, ele lança-se nesta aventura.

7.2. A base de dados

O nosso comerciante cria o seguinte esboço da interface web inicial que gostaria de ter:

Image

Haveria dois tipos de utilizadores:

  • administradores, que poderiam realizar todas as operações na tabela de artigos (adicionar, alterar, eliminar, consultar, etc.). Estes poderão utilizar todos os elementos do menu acima. Em particular, poderão emitir qualquer consulta SQL através da opção [Requête SQL].
  • os utilizadores normais (que não são administradores), que teriam direitos restritos: direitos de adicionar, alterar, eliminar e consultar. Podem ter apenas alguns desses direitos, como, por exemplo, apenas o direito de consulta.

Dado que existem vários tipos de utilizadores da base de dados que não têm os mesmos direitos, é necessária uma autenticação. É por isso que a página inicial começa com este processo. Para saber quem é quem e quem tem o direito de fazer o quê, serão utilizadas duas tabelas: USERS e DROITS. A tabela USERS teria a seguinte estrutura:

login
O login do utilizador identifica-o de forma única. Este campo é a chave primária da tabela.
mdp
a palavra-passe do utilizador em texto simples
admin 
o caractere «y» (sim) se o utilizador for administrador; caso contrário, o caractere «n» (não).

O conteúdo da tabela poderia ser o seguinte:

Image

A tabela DROITS especifica os direitos dos utilizadores que não são administradores e que constam da tabela USERS. A sua estrutura é a seguinte:

login
login do utilizador, que o identifica de forma única.
Este campo é uma chave estrangeira da tabela DROITS e faz referência
à coluna «login» da tabela USERS.
table
o nome da tabela sobre a qual o utilizador tem direitos.
ajouter
o carácter «y» (yes) se o utilizador tiver direito de adição na tabela,
caso contrário, o caractere «n» (não).
modifier
direito de modificação: «y» ou «n»
supprimer
direito de eliminar: «y» ou «n»
consulter
direito de consulta: «y» ou «n»

O conteúdo da tabela poderia ser o seguinte:

Image

Observações:

  • Um utilizador U que conste na tabela USERS e não conste na tabela DROITS não tem quaisquer direitos.
  • No nosso exemplo, os utilizadores terão acesso apenas a uma única tabela, a tabela ARTICLES. Mas o nosso comerciante, previdente, adicionou, no entanto, o campo «tabela» à estrutura da tabela DROITS, a fim de se dar a possibilidade de adicionar posteriormente novas tabelas à sua aplicação.
  • Por que razão gerir direitos nas nossas próprias tabelas, se partimos do princípio de que iremos utilizar uma base de dados MySQL capaz, por si só (e melhor do que nós), de gerir esses direitos nas suas próprias tabelas? Simplesmente porque o nosso comerciante não possui direitos de administração sobre a base de dados MySQL que lhe permitiriam criar utilizadores e atribuir-lhes direitos. Não nos esqueçamos, de facto, de que a base de dados MySQL está alojada num fornecedor de acesso e que o comerciante é apenas um simples utilizador da mesma, sem quaisquer direitos de administração (felizmente). No entanto, possui todos os direitos sobre uma base de dados chamada dbarticles, à qual acede, por enquanto, com o nome de utilizador admarticles e a palavra-passe mdparticles. É nesta base de dados que se encontram todas as tabelas da aplicação.

A tabela ARTICLES reúne as informações sobre os artigos vendidos pelo comerciante. A sua estrutura é a seguinte:

code
código do artigo - chave primária da tabela
- exatamente 4 caracteres
nom
nome do artigo
prix
preço
stockActuel
o nível atual do seu stock
stockMinimum
o nível abaixo do qual deve ser efetuada uma encomenda
de reabastecimento deve ser efetuada

O seu conteúdo, utilizado inicialmente como teste, poderia ser o seguinte:

Image

7.3. As restrições do projeto

O comerciante está aqui a migrar uma aplicação local ACCESS para uma aplicação Web. Não sabe o que será desta aplicação nem como irá evoluir. No entanto, gostaria que a nova aplicação fosse fácil de utilizar e escalável. Por esta razão, o seu consultor informático concebeu, durante a conceção das tabelas, a possibilidade de haver:

  • vários utilizadores com direitos diferentes: isto permitirá ao comerciante delegar certas tarefas a outras pessoas sem, por isso, lhes conceder direitos de administração
  • no futuro, outras tabelas além da tabela ARTICLES

O mesmo consultor faz outras propostas:

  • ele sabe que, no desenvolvimento de software, é necessário separar claramente as camadas de apresentação das camadas de processamento. A arquitetura de uma aplicação web é frequentemente a seguinte:

A interface do utilizador é, neste caso, um navegador web, mas poderia ser também uma aplicação autónoma que, através da rede, enviasse pedidos HTTP ao serviço web e formatasse os resultados que este lhe enviasse. A lógica da aplicação é constituída pelos scripts que processam os pedidos do utilizador, neste caso os scripts PHP. A fonte de dados é frequentemente uma base de dados, mas também pode ser um diretório LDAP ou um serviço web remoto. É do interesse do programador manter uma grande independência entre estas três entidades, para que, caso uma delas mude, as outras duas não tenham de mudar, ou apenas minimamente. O consultor informático do comerciante faz então as seguintes propostas:

  • Colocaremos a lógica de negócio da aplicação numa classe PHP. Assim, o bloco [Logique applicative] acima será constituído pelos seguintes elementos:

No bloco [Logique Applicative], será possível distinguir

  • o bloco [IE=Interface d'Entrée], que é a porta de entrada da aplicação. É o mesmo independentemente do tipo de cliente.
  • o bloco [Classes métier], que agrupa as classes necessárias à lógica da aplicação. Estas são independentes do cliente.
  • o bloco dos geradores de páginas de resposta [IS1 IS2 ... IS=Interface de Sortie]. Cada gerador é responsável por formatar os resultados fornecidos pela lógica da aplicação para um determinado tipo de cliente: código HTML para um navegador ou um telemóvel (WAP), código XML para uma aplicação autónoma, etc.

Este modelo garante uma boa independência em relação aos clientes. Quer o cliente mude, quer se pretenda alterar a forma de apresentar os resultados, serão os geradores de saída [IS] que terão de ser criados ou adaptados.

  • Numa aplicação web, a independência entre a camada de apresentação e a camada de processamento pode ser melhorada através da utilização de folhas de estilo. Estas regem a apresentação de uma página web num navegador. Para alterar essa apresentação, basta alterar a folha de estilo associada. Não é necessário alterar a lógica de processamento. Por isso, utilizaremos aqui uma folha de estilo.
  • No diagrama acima, é a classe de negócio que fará a interface com a fonte de dados. Por hipótese, esta fonte é, neste caso, uma base de dados MySQL. Para permitir a migração para outra base de dados, utilizar-se-á a biblioteca PEAR, que oferece classes de acesso a bases de dados independentes do tipo real destas. Assim, se o nosso comerciante se tornar suficientemente próspero para poder instalar um servidor web IIS da Microsoft na sua empresa, poderá substituir a base de dados MySQL pelo SQL Server sem ter de alterar (ou com muito poucas alterações) a classe de negócio.

7.4. A classe «artigos»

A classe «artigos» poderia ser definida da seguinte forma:

<?php

     // classe de artigos que opera numa base de dados de artigos composta pelas seguintes tabelas
     // artigos: (código, nome, preço, stockActuel, stockMinimum)
     // utilizadores: (nome de utilizador, palavra-passe, administrador)
     // direitos: (nome de utilizador, tabela, adicionar, alterar, eliminar, consultar)

     // é o utilizador da turma que deve fornecer o nome de utilizador e a palavra-passe que permitem realizar todas as operações na base de dados
     // por isso, já possui todos os direitos sobre a base de dados. Isto implica que não é necessário tomar
     // precauções de segurança específicas neste caso

     // bibliotecas
  require_once 'DB.php';

  class articles{

           // atributos
    var $sDSN;                        // a cadeia de ligação
          var $sDatabase;            // o nome da base de dados
    var $oDB;                        // ligação à base de dados
    var $aErreurs;                // lista de erros
    var $oRésultats;            // resultado de uma consulta SELECT
        var $connecté;                // valor booleano que indica se está ou não ligado à base de dados
        var $sQuery;                    // a última consulta executada
    var $sUser;                    // identidade do utilizador da ligação
    var $bAdmin;                    // tem o valor «verdadeiro» se o utilizador for administrador
    var $dDroits;                // o dicionário dos seus direitos: tabela ->> array(consultar, adicionar, eliminar, modificar)

     // construtor
    function articles($dDSN,$sUser,$sMdp){

             // $dDSN: dicionário que define a ligação a estabelecer
       // $dDSN['sgbd']: o tipo de SGBD ao qual é necessário ligar-se
       // $dDSN['host']: o nome do servidor que o aloja      
       // $dDSN['database']: o nome da base de dados à qual é necessário ligar-se      
       // $dDSN['admin']: o nome de utilizador do proprietário da base de dados à qual é necessário ligar-se
       // $dDSN['mdpadmin']: a sua palavra-passe
       // $sUser: o nome de utilizador do utilizador que pretende utilizar a base de dados de artigos
       // $sMdp: a sua palavra-passe

       // cria em $oDB uma ligação à base de dados definida por $dDSN sob a identidade de $dDSN['admin']
       // se a ligação for bem-sucedida e o utilizador $sUser for autenticado  
           // carrega os direitos em $bAdmin e $dDroits os direitos do utilizador $sUser
           // insere em $sDSN a cadeia de ligação à base de dados
           // insere em $sDataBase o nome da base de dados à qual se estabelece a ligação
         // define $connecté como verdadeiro
       // se a ligação falhar ou se o utilizador $sUser não for identificado corretamente
           // insere as mensagens de erro adequadas na lista $aErreurs
         // encerra a ligação, se necessário
         // define $connecté como falso 

  ...
    }//criador

    // ------------------------------------------------------------------
    function connect(){
             // (re)ligação à base
...
    }//ligar

    // ------------------------------------------------------------------
    function disconnect(){
      // encerrar a ligação à base $sDSN
...
    }//desligar

    // -------------------------------------------------------------------
    function execute($sQuery,$bAdmin){
            // $sQuery: consulta a executar
       // $bAdmin: verdadeiro se for solicitada a execução como administrador
...
    }//executar

    // --------------------------------------------------------------------------
    function addArticle($dArticle){
         // adiciona um artigo $dArticle (código, nome, preço, stockActuel, stockMinimum) à tabela de artigos
   ...
    }//adicionar          

    // ----------------------------------------------------------------------
    function modifyArticle($dArticle){
             // altera um artigo $dArticle (código, nome, preço, stockActuel, stockMinimum) da tabela de artigos
...
    }//atualização

    // ----------------------------------------------------------------------
    function deleteArticle($sCode){
             // elimina um artigo da tabela de artigos
       // cujo código é $sCode
...
    }//eliminar

    // ----------------------------------------------------------------------
    function vérifierArticle(&$dArticle){
         // verifica a validade de um artigo $dArticle (código, nome, preço, stockActuel, stockMinimum)
...
    }//verificar

    // --------------------------------------------------------------------------
    function selectArticles($dQuery){
             // executa uma consulta SELECT na tabela de artigos
       // esta tem três componentes
       // lista de colunas em $dQuery['colonnes']
       // filtragem em $dQuery['where']
       // ordem de apresentação em $dQuery['orderby']
...
    }//selectArticles            

        // --------------------------------
    function existeArticle($sCode){
         // retorna TRUE se o artigo com o código $sCode existir na tabela de artigos
...
    }//existeArticle

    // --------------------------------------
    function existeUser($sUser,$sMdp){
             // verificação da existência do utilizador $sUser com a palavra-passe $sMdp
       // retorna (int $iErreur, string $sAdmin, tabela de hash $dDroits)
       // $iErreur = -1 em caso de qualquer erro na exploração da base de dados — a lista $aErreurs é então preenchida
       // $iErreur = 1 se o utilizador não for encontrado (ausente ou palavra-passe incorreta)
       // $iErreur = 2 se o utilizador existir, mas não tiver quaisquer direitos na tabela de direitos
       // $iErreur = 3 se o utilizador existir e for administrador
       // $iErreur = 0 se o utilizador existir e não for administrador
       // $sAdmin = «y» se o utilizador existir e for administrador ($iErreur == 3); caso contrário, é igual à cadeia vazia
       // $dDroits é o dicionário de direitos do utilizador se este não for administrador ($iErreur==0)
       // caso contrário, é uma tabela vazia
       // as chaves do dicionário são as tabelas sobre as quais o utilizador tem direitos
       // o valor associado a esta tabela é, por sua vez, um dicionário cujas chaves são os direitos
       // (consultar, adicionar, alterar, eliminar) e os valores são as cadeias «y» (sim) ou «n» (não), conforme o caso
...
    }//existeUser

    // --------------------------------------
    function getCodes(){
         // gera a tabela de códigos
....
    }//getCodes    

  }//classifica
?>      

Comentários

  • A classe «artigos» utiliza a biblioteca PEAR::DB para os seus acessos à base de dados, daí o comando
require_once 'DB.php';

Esta inclusão pressupõe que o script DB.php se encontre num dos diretórios da opção include_path do ficheiro de configuração de PHP.

  • O construtor precisa de saber a que base de dados se liga e com que identidade. Estas informações são-lhe fornecidas no dicionário $dDSN. Recorde-se que a hipótese inicial era que a base de dados se chamava dbarticles e que pertencia a um utilizador chamado admarticles, com a palavra-passe mdparticles. Recorde-se também que esta aplicação permite vários utilizadores com direitos diferentes. Existe aqui uma ambiguidade a esclarecer. A ligação é efetivamente estabelecida com a identidade admarticles e, no final,é sob esta identidade que serão realizadas todas as operações na base de dados dbarticles, uma vez que é o único nome que o SGBD MySQL conhece e que possui direitos suficientes para gerir a base de dados dbarticles. Para «simular» a existência de diferentes utilizadores, faremos com que o utilizador admarticles funcione com os direitos do utilizador cujo nome de utilizador ($sUser) e palavra-passe ($sMdp) são passados como parâmetros ao construtor. Assim, antes de realizar uma operação na base de dados de artigos, verificar-se-á se o utilizador ($sUser, $sMdp) possui efetivamente as autorizações necessárias para a realizar. Se for o caso, será o utilizador admarticles a realizá-la em seu nome.
  • O nome de utilizador e a palavra-passe do administrador da base de dados de artigos devem ser passados para o construtor. Trata-se de uma precaução sensata. Se estas duas informações fossem inseridas «fixamente» no código da classe, qualquer utilizador da classe poderia facilmente fazer-se passar por administrador da base de dados de artigos. Com efeito, uma classe PHP não está protegida. Além disso, o atributo $bAdmin da classe, que indica se o utilizador ($sUser, $sMdp) para quem se está a trabalhar é administrador ou não, poderia muito bem ser definido diretamente a partir do exterior, como no exemplo seguinte:
$oArticles=new articles($dDSN,$sUser,$sMdp)
// aqui, o $sUser foi reconhecido como um utilizador não administrador da base de dados
$oArticle->bAdmin=TRUE;
// agora $sUser tornou-se administrador

PHP não é JAVA nem C# e uma classe PHP é apenas uma estrutura de dados um pouco mais avançada do que um dicionário, mas que não oferece a segurança de uma verdadeira classe, na qual o atributo bAdmin teria sido declarado privado ou protegido, tornando impossível a sua modificação a partir do exterior. Como o utilizador da classe tem de conhecer o nome de utilizador e a palavra-passe do administrador da base de dados de artigos, apenas este último pode utilizar a classe. A operação anterior deixa, portanto, de ter qualquer interesse para ele. A classe existe apenas para lhe facilitar o desenvolvimento. Uma consequência importante é que não há necessidade de tomar precauções de segurança. Mais uma vez, quem utiliza a classe articles é necessariamente administrador da base de dados de artigos.

  • A classe gere os erros de ligação à base de dados ou quaisquer outros erros de forma única, preenchendo o atributo $aErreurs com a(s) mensagem(ns) de erro. Após cada operação, o utilizador da classe deve, portanto, verificar esta lista.
  • Os métodos addArticle, updateArticle, deleteArticle, selectArticles e execute decorrem diretamente do modelo da interface web apresentado anteriormente. Correspondem, de facto, às opções do menu proposto. Os métodos addArticle e modifyArticle baseiam-se no método vérifierArticle para verificar se o artigo que vai ser adicionado ou alterado contém dados corretos. Na mesma linha, o método existeArticle permite verificar se não se está prestes a adicionar um artigo que já existe. Poder-se-ia prescindir deste método se se utilizasse uma tabela de artigos em que o código fosse a chave primária. Nesse caso, seria o próprio SGBD a sinalizar o fracasso da adição devido a uma duplicação. Provavelmente, fá-lo-á através de uma mensagem de erro pouco legível e em inglês.
  • Um artigo a modificar ou a eliminar será identificado pelo seu código, que é único. O método getCodes permite obter todos esses códigos.
  • O método disconnect encerra a ligação à base de dados, ligação essa que foi aberta durante a criação do objeto. Não se percebe aqui a utilidade do método connect, que irá recriar uma ligação com a base de dados. Isto permitirá abrir e encerrar essa ligação à vontade com o mesmo objeto. A utilidade só se torna evidente em conjunto com a aplicação web. Esta irá criar um objeto «artigos» que irá armazenar numa sessão. Embora a sessão consiga conservar a maioria dos atributos do objeto ao longo das sucessivas trocas cliente-servidor, não é capaz, no entanto, de manter o atributo que representa a ligação aberta. Por conseguinte, esta terá de ser reaberta a cada nova troca cliente-servidor. Solicitar-se-á uma ligação persistente para que a ligação aberta seja armazenada num conjunto de ligações e permaneça aberta de forma permanente. Assim, quando o script solicitar uma nova ligação, esta será recuperada do conjunto de ligações. Chega-se, portanto, ao mesmo resultado que se a sessão tivesse conseguido memorizar a ligação aberta.
  • O método existeUser permite ao fabricante verificar se o utilizador $sUser, identificado pela palavra-passe $sMdp, existe efetivamente. Se sim, o método permite determinar se é administrador ou não (conforme indicado na tabela USERS) e armazena essa informação no atributo $bAdmin. Se não for administrador, o método irá recuperar os seus direitos da tabela DROITS e colocá-los no atributo $dDroits, que é um dicionário de dupla indexação: $dDroits[$table][$droit] vale 'y' se o utilizador $sUser tiver o direito $droit sobre a tabela $table e vale 'n' caso contrário.

Escreva a classe «artigos». Os acessos à base de dados serão efetuados com a ajuda da biblioteca PEAR::DB, que permite ignorar o tipo exato da base de dados.

7.5. A estrutura da aplicação WEB

Agora que temos a classe «de negócio» para a gestão da base de artigos, podemos utilizá-la em diferentes ambientes. Propõe-se aqui utilizá-la numa aplicação web. Vamos explorá-la através destas diferentes páginas:

7.5.1. A página padrão da aplicação

Voltemos à página inicial já apresentada:

1234

Image

Todas as páginas da aplicação terão a estrutura acima, a de uma tabela com duas linhas e três colunas, composta por quatro zonas:

  • a zona 1 constitui a primeira linha da tabela. Está reservada ao título, eventualmente acompanhado por uma imagem. As três colunas da linha estão aqui fundidas.
  • a segunda linha tem três zonas, uma zona por coluna:
    • a zona 2 contém as opções do menu. Por sua vez, contém uma tabela com uma coluna e várias linhas. As opções do menu são colocadas nas linhas da tabela.
    • a zona 3 está vazia e serve apenas para separar as zonas 2 e 4. Teria sido possível proceder de forma diferente para realizar esta separação.
    • A zona 4 é aquela que contém a parte dinâmica da página. É esta parte que muda de uma ação para outra, mantendo-se as restantes idênticas.

O script PHP que gera este modelo de página chamar-se-á main.php e poderá ser o seguinte:


<html>
  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>
  <body background="<?php echo $dConfig['urlBackGround'] ?>">
    <table>
      <tr height="60">
        <td colspan="3" align="left" valign="top" >
          <h1><?php echo $main["title"] ?></h1>
        </td>
      </tr>
      <tr>
        <td>
          <table>
            <tr>
              <td class="menutitle" >
                                    <a href="<?php echo  $main["liens"]["login"] ?>" ?>Authentification</a>
              </td>
            </tr>
            <tr>
                <td><br /></td>
            </tr>
            <tr>
              <td class="menutitle" >
                Utilisation
              </td>
            </tr>
            <tr height="10"></tr>
            <tr>
              <td class="menublock" >
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo  $main["liens"]["addArticle"] ?>" ?>
                  Ajouter un article
                </a>
                 </td>
            </tr>
            <tr>
              <td class="menublock" >                    
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["updateArticle"] ?>">
                  Modifier un article
                </a>
                    </td>
            </tr>
              <td class="menublock" >                
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["deleteArticle"] ?>">
                  Supprimer un article
                </a>
              </td>
            </tr>
            <tr>
              <td class="menublock" >
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["selectArticle"] ?>">
                  Lister des articles
                </a>
              </td>
            </tr>
            <tr>
                <td><br /></td>
            </tr>                
            <tr>
              <td class="menutitle" >
                Administration
              </td>
            </tr>
            <tr height="10"></tr>
            <tr>
              <td class="menublock" >                
                <img alt="-" src="../images/radio.gif" />
                <a href="<?php echo $main["liens"]["sql"] ?>" >
                  Requête SQL
                </a>
              </td>
            </tr>
          </table>
        </td>
            <td>  
            <img alt="/" src="../images/pix.gif" width="10" height="1" />
            </td>
            <td>
            <fieldset>
              <legend><?php echo $main["légende"] ?></legend>
            <?php
                include $main["contenu"];
            ?>
          </fieldset>
        </td>
      </tr>
    </table>
  </body>
</html>

As áreas configuradas da página foram destacadas na lista acima. A página-modelo é configurada de várias formas:

  • através de um dicionário $main com as seguintes chaves:
    • title: título a inserir na zona 1 da página
    • links: dicionários dos links a gerar na coluna do menu. Estes links estão associados às opções do menu da zona 2
    • conteúdo: URL da página a apresentar na zona 4
  • através de um dicionário $dConfig que reúne informações extraídas de um ficheiro de configuração da aplicação denominado config.php
  • por meio de classes que fazem parte da folha de estilo utilizada pela página:
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />

A página utiliza aqui as seguintes classes de estilo:

  • menutitle: para uma opção principal do menu
  • menublock: para uma opção secundária do menu

Alterar um dos parâmetros altera o aspeto da página. Assim, alterar $main['title'] alterará o título da zona 1.

7.5.2. O processamento típico de um pedido de um cliente

O cliente interage com a aplicação através dos links da zona 2 da página-modelo. Estes links serão do seguinte tipo:

apparticles.php?action=xx&phase=y&PHPSESSID=zzzzzzzzzzzz
action
indica a ação em curso entre as seguintes:
authentifier
autenticação do cliente
selectArticles
seleção de artigos (consulta)
updateArticle
alteração de um artigo
deleteArticle
eliminação de um artigo
sql
Envio de qualquer solicitação SQL (administrador)
phase
uma ação pode ser realizada em várias etapas — indica a etapa em curso
PHPSESSID
token de sessão quando esta é iniciada — permite ao servidor recuperar informações armazenadas na sessão durante as trocas anteriores

Da mesma forma, o atributo «action» nos formulários terá o mesmo formato. Por exemplo, na página inicial existe um formulário de início de sessão na zona 4. A baliza HTML deste formulário está definida da seguinte forma:

<form name="frmLogin" method="post" action="apparticles.php?action=authentifier&phase=1">

O processamento do pedido do cliente é realizado pelo script principal da aplicação, denominado apparticles.php. A sua função é construir a resposta para o cliente. Procederá sempre da mesma forma:

  • com base no nome da ação e na fase em curso, o sistema encaminhará o pedido para uma função especializada. Esta função processará o pedido e gerará a página de resposta adequada. Para cada pedido do cliente, podem existir várias páginas de resposta possíveis: página1, página2, …, página n. Estas páginas contêm informações que têm de ser calculadas pela função. Trata-se, portanto, de páginas parametrizadas. Estas serão geradas pelos scripts page1.php, page2.php, ..., pagen.php.
  • Por uma questão de uniformidade, as partes variáveis das páginas a apresentar na zona 4 da página-modelo serão também colocadas no dicionário $main.

Suponhamos que, em resposta a um pedido, o servidor tenha de enviar a página pagex.php ao cliente. Procederá da seguinte forma:

  • colocará no dicionário $main os valores necessários para a página pagex.php
  • colocará em $main['contenu'] — que designa o URL da página a apresentar — na zona 4 da página-modelo, o URL de pagex.php
  • solicitará a exibição da página-modelo com a instrução
include "main.php";

A página-modelo será então apresentada com, na zona 4, o código do script pagex.php, que será avaliado para gerar o conteúdo da zona 4. Recorde-se que esta é uma simples célula de uma tabela. Por isso, o código HTML gerado por pagex.php não deve começar pelas balizas <HTML>, <HEAD>, <BODY>, ... Estas já foram emitidas no início da página-modelo. Eis, por exemplo, como poderia ser o script login.php que gera a zona 4 da página inicial:


<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
    <table>
        <tr>
            <td>login</td>
            <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
        </tr>
        <tr>
            <td>mot de passe</td>
            <td><input type="password" value="" name="txtMdp" class="text"></td>
      <td><input type="submit" value="Connexion" class="submit"></td>      
        </tr>
    </table>
</form>

Como se pode ver, a página:

  • está reduzida a um formulário
  • é configurada tanto pelo dicionário $main como pela folha de estilo.

7.5.3. O ficheiro de configuração

É sempre aconselhável configurar as aplicações o mais possível, para evitar ter de recorrer ao código simplesmente porque se decidiu, por exemplo, alterar o caminho de um script ou de uma imagem. A aplicação principal apparticles.php carregará, portanto, um ficheiro de configuração config.php ao iniciar:

     // a carregar o ficheiro de configuração
  include "config.php";

Neste ficheiro, serão incluídas diretivas de configuração destinadas ao PHP e inicializações de variáveis globais:

<?php

     // configuração do PHP
  ini_set("register_globals","off");
  ini_set("display_errors","off");
  ini_set("expose_php","off");
    ini_set("session.use_cookies","0");    // sem cookies

     // configuração básica dos artigos
    $dConfig["DSN"]=array(
        "sgbd"=>"mysql",
        "admin"=>"admarticles",
        "mdpadmin"=>"mdparticles",
        "host"=>"localhost",
        "database"=>"dbarticles"
    );

   // URL das páginas
    $dConfig['urlBackGround']="../images/standard.jpg";  
  $dConfig["urlPageStyle"]="mystyle.css";  
  $dConfig["urlAppArticles"]="apparticles.php";
  $dConfig["urlPageMain"]="main.php";
  $dConfig["urlPageLogin"]="login.php";
  $dConfig["urlPageErreurs"]="erreurs.php";
  $dConfig["urlPageInfos"]="infos.php";
  $dConfig["urlPageAddArticle"]="addarticle.php";
  $dConfig["urlPageUpdateArticle1"]="updatearticle1.php";
  $dConfig["urlPageUpdateArticle2"]="updatearticle2.php";
  $dConfig["urlPageDeleteArticle1"]="deletearticle1.php";
  $dConfig["urlPageDeleteArticle2"]="deletearticle2.php";
  $dConfig["urlPageSelectArticle1"]="selectarticle1.php";
  $dConfig["urlPageSelectArticle2"]="selectarticle2.php";
  $dConfig["urlPageSQL1"]="sql1.php";
  $dConfig["urlPageSQL2"]="sql2.php";
  $dConfig["urlPageSQL3"]="sql3.php";

   // links da página principal
  $main["liens"]["login"]="$sUrlAppArticles?action=authentifier&phase=0";  
  $main["liens"]["addArticle"]="$sUrlAppArticles?action=addArticle&phase=0";
  $main["liens"]["updateArticle"]="$sUrlAppArticles?action=updateArticle&phase=0";
  $main["liens"]["deleteArticle"]="$sUrlAppArticles?action=deleteArticle&phase=0";
  $main["liens"]["selectArticle"]="$sUrlAppArticles?action=selectArticle&phase=0";
  $main["liens"]["sql"]="$sUrlAppArticles?action=sql&phase=0";

   // guardamos $main na configuração
  $dConfig["main"]=$main;    
?>

7.5.4. A folha de estilo associada à página-modelo

Vimos que a resposta do servidor tinha um formato único: main.php. Foi possível observar que este script produz uma página em bruto, sem efeitos de apresentação. Isto é positivo por várias razões:

  • o programador não tem de se preocupar com a apresentação gráfica da página que está a criar. Na verdade, nem sempre possui as competências necessárias para criar páginas graficamente apelativas. Assim, pode concentrar-se inteiramente no código.
  • a manutenção dos scripts é facilitada. Se estes incluíssem atributos de apresentação, nem a estrutura do código, nem a da apresentação ficariam claras. O aspeto gráfico das páginas é frequentemente delegado a um designer gráfico. Este provavelmente não gostaria de ter de procurar num script que não compreende onde se encontram os atributos de apresentação que tem de alterar.

No entanto, é necessário ter em conta o aspeto gráfico das páginas. Com efeito, é isso que atrai os internautas para um site. Aqui, a apresentação é delegada a uma folha de estilo. A página main.php indica no seu código a folha de estilo a utilizar para a apresentar:

  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>

A folha de estilo utilizada neste documento é a seguinte:

BODY {
    background : url(../images/standard.jpg);
    border : 2px none #FFDAB9;
    font-family : Garamond;
    font-size : 16px;
    margin-left : 0px;
    padding-left : 20px;
}

INPUT {
    background : #EEE8AA;
    border : 1px solid #EE82EE;
    font-family : Garamond;
    font-size : 18px;
}

INPUT.submit{
    font-family : "Times New Roman";
    font-size : 16px;
    background : #FA8072;
    border : 2px double Green;
    font-weight : bold;
    text-align : center;
    vertical-align : middle;
    cursor : pointer;
}

TD.menutitle{
    background-image : url(../images/menugelgd.gif);
    height : 23px;
    text-align : center;
    vertical-align : middle;
    background : url(../images/menugelgd.gif) no-repeat center;
}

TD.menublock{
    background : url(../images/bandegrismenugd.gif) repeat-x;
    text-align : left;
    vertical-align : middle;
}

A {
    font-family : "Comic Sans MS";
    color : #FF7F50;
    font-size : 15px;
    text-decoration : none;
}

A:HOVER {
    background : #FFA07A;
    color : Red;
}

FIELDSET {
    border : 1px solid #A0522D;
    background : #FFE4C4;
    margin : 10px 10px 10px 10px;
    padding-left : 10px;
    padding-right : 10px;
    padding-bottom : 10px;
}

LEGEND{
    background : #FFA500;
}

TH {
    background : #228B22;
    text-align : center;
    vertical-align : middle;
}

TD.libellé{
    border : 1px solid #008B8B;
    color : #339966;
}

H1 {
    font : bold 20px/30px Garamond;
    color : #FF7F50;
    background : #D1E1F8;
    background-attachment : fixed;
    text-align : center;
    vertical-align : middle;
    font-family : Garamond;
}

SELECT.TEXT {
    background : #6495ED;
    text-align : center;
    color : Aqua;
}

Não entraremos em pormenores sobre esta folha de estilo. Aceitá-la-emos tal como está. Mais adiante, veremos como criá-la e modificá-la. Existem programas para o efeito. No entanto, indicamos aqui a função dos atributos de apresentação utilizados na folha:

Atributo:
determina a apresentação da baliza HTML:
BODY
<BODY>
H1
<H1> (Cabeçalho 1)
A
<A> (Âncora)
A:HOVER
define os atributos de apresentação da âncora quando o utilizador passa o cursor por cima dela
FIELDSET
<FIELDSET> - esta baliza não é reconhecida por todos os navegadores
LEGEND
<LEGEND> - esta baliza não é reconhecida por todos os navegadores
INPUT
<INPUT>
INPUT.TEXT
<INPUT class="TEXT">
INPUT.SUBMIT
<INPUT class="SUBMIT">
TH
<TH> (Cabeçalho da tabela)
TD.menutitle
<TD class="menutitle"> (Dados da tabela)
TD.menublock
<TD class="menublock">
TD.libellé
<TD class="libellé">

Vejamos, através de um exemplo, como estas regras de apresentação podem ser definidas. Neste exemplo, utilizaremos o software TopStyle Lite, disponível gratuitamente no site http://www.bradsoft.com. Depois de carregada a folha de estilo, surge uma janela com três áreas:

  1. uma área de edição de texto. Os atributos de apresentação podem ser definidos manualmente, desde que se conheçam as regras de escrita das folhas de estilo, que seguem uma norma denominada CSS (Cascading Style Sheets).
  2. a zona 2 apresenta as propriedades editáveis do atributo que está a ser criado. Este é o método mais simples. Evita a necessidade de conhecer o nome exato dos atributos de apresentação, que são muito numerosos
  3. A zona 3 mostra o aspeto visual do atributo que está a ser criado

Na zona 1 acima, copiem e colem o atributo INPUT.submit para um atributo INPUT.fantaisie. Este atributo definirá a apresentação da baliza HTML <INPUT class="fantaisie">

Vamos utilizar a zona 2 para alterar algumas das propriedades do atributo INPUT.fantaisie:

A partir de agora, qualquer baliza <INPUT ... class="fantaisie"> encontrada numa página HTML associada à folha de estilo anterior será apresentada tal como no exemplo da zona 3 acima.

As folhas de estilo têm uma grande utilidade. A sua utilização permite alterar o «aspecto» de uma aplicação web, modificando-a apenas num único ponto: a sua folha de estilo. As folhas de estilo não são reconhecidas pelos navegadores mais antigos. A diretiva <link ..> abaixo será ignorada por alguns deles:

  <head>
      <title>Gestion d'articles</title>
      <link type="text/css" href="<?php echo $dConfig['urlPageStyle'] ?>" rel="stylesheet" />
   </head>

Na nossa aplicação, isto resultará na seguinte página inicial:

Image

Temos aqui uma página mínima, sem elementos gráficos. Poderia ser pior. Algumas versões de navegadores reconhecem as folhas de estilo, mas interpretam-nas incorretamente. Podemos, então, ter uma página desfigurada e inutilizável. Coloca-se, portanto, a questão do tipo de navegador do cliente. Existem técnicas que ajudam a determinar o tipo de navegador do cliente. No entanto, não são totalmente fiáveis. É possível, então, escrever diferentes folhas de estilo para diferentes navegadores ou mesmo escrever uma versão sem folha de estilo para os navegadores que as ignoram. É claro que isto torna a tarefa de desenvolvimento mais complexa. Este problema importante foi aqui ignorado.

Com as folhas de estilo, podemos pensar em oferecer um ambiente personalizado aos utilizadores da nossa aplicação. Poderíamos apresentar-lhes uma página com vários estilos de apresentação possíveis. Eles poderiam escolher aquele que melhor lhes convier. Essa escolha poderia ser guardada numa base de dados. Quando o utilizador voltar a iniciar sessão, poderíamos então iniciar a aplicação com a folha de estilo que ele preferiu.

7.5.5. O módulo de entrada da aplicação

Os clientes só conhecerão o módulo de entrada da aplicação: apparticles.php. As linhas gerais do seu funcionamento são as seguintes:

  • O pedido do cliente é recolhido e analisado. Este pode ou não estar configurado. Quando está parametrizada, os parâmetros esperados são os seguintes: action=[action]&phase=[phase]&PHPSESSID=[PHPSESSID]
  • Se o pedido não estiver configurado ou se os parâmetros recuperados não forem os esperados, o servidor envia como resposta a página de autenticação (nome de utilizador, palavra-passe). Assim que o utilizador se identificar corretamente, é criada uma sessão. Esta servirá para armazenar informações ao longo das trocas entre o cliente e o servidor.
  • Se uma solicitação for reconhecida corretamente, é processada por um módulo que depende tanto da ação como da fase em curso.
  • Todos os acessos à base de dados são efetuados através da classe de negócio articles.php.
  • O processamento de um pedido termina sempre com o envio ao cliente da página main.php, na qual foi especificado, em $main['contenu'], oURL da página a inserir na zona 4 da página-modelo.

A estrutura do script apparticles.php poderia ser a seguinte:

<?php
     // gestão de uma tabela de artigos
  include "config.php";
  include "articles.php";  

  // ação a realizar
  $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
  $sAction=strtolower($sAction);
   // fase eventual
  $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

  // sessão
  session_start();
  $dSession=$_SESSION["session"];

     // existe alguma sessão em curso?
  if(! isset($dSession)){
      // autenticação do utilizador
    if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
    if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
    if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);
     // pedido incorreto
    authentifier_0($dConfig);        
  }//if - sem sessão

     // recuperar a sessão
  $dSession=unserialize($dSession);

     // processamento do pedido
     // ----- autenticação
  if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
  if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);
  if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);  
     // ----- adição de artigo
  if($sAction=="addarticle" && $sPhase=="0") addArticle_0($dConfig,$dSession);
  if($sAction=="addarticle" && $sPhase=="1") addArticle_1($dConfig,$dSession);
  if($sAction=="addarticle" && $sPhase=="2") addArticle_2($dConfig,$dSession);
     // ----- atualização de artigo
  if($sAction=="updatearticle" && $sPhase=="0") updateArticle_0($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="1") updateArticle_1($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="2") updateArticle_2($dConfig,$dSession);
  if($sAction=="updatearticle" && $sPhase=="3") updateArticle_3($dConfig,$dSession);
     // ----- eliminação de artigo
  if($sAction=="deletearticle" && $sPhase=="0") deleteArticle_0($dConfig,$dSession);
  if($sAction=="deletearticle" && $sPhase=="1") deleteArticle_1($dConfig,$dSession);
  if($sAction=="deletearticle" && $sPhase=="2") deleteArticle_2($dConfig,$dSession);
    // ----- consulta de artigos
  if($sAction=="selectarticle" && $sPhase=="0") selectArticle_0($dConfig,$dSession);
  if($sAction=="selectarticle" && $sPhase=="1") selectArticle_1($dConfig,$dSession);
  if($sAction=="selectarticle" && $sPhase=="2") selectArticle_2($dConfig,$dSession);
     // ----- envio de um pedido SQL
  if($sAction=="sql" && $sPhase=="0") sql_0($dConfig,$dSession);
  if($sAction=="sql" && $sPhase=="1") sql_1($dConfig,$dSession);
  if($sAction=="sql" && $sPhase=="2") sql_2($dConfig,$dSession);


    // ação incorreta - é apresentada a página de autenticação
  session_destroy();
  authentifier_0($dConfig,"0");
...
?>

É de salientar o seguinte:

  • as funções que tratam de um pedido específico do cliente terminam com a geração da página de resposta e com uma instrução «exit» que encerra a execução do script apparticles.php. Por outras palavras, não se «retorna» destas funções.
  • As funções aceitam um ou dois parâmetros:
    • $dConfig é um dicionário que contém informações provenientes do ficheiro de configuração config.php. Todas as funções o utilizam.
    • $dSession é um dicionário que contém informações da sessão. Só existe quando a sessão foi criada, ou seja, após a autenticação bem-sucedida do utilizador. É por isso que as funções de autenticação não têm este parâmetro.

7.5.6. A página de erros

Qualquer aplicação de software deve saber gerir corretamente os erros que possam ocorrer. Uma aplicação web não escapa a esta regra. Aqui, em caso de erro, iremos colocar a seguinte página erreurs.php na zona 4 da página-modelo:

Les erreurs suivantes se sont produites :
<ul>
    <?php
        for($i=0;$i<count($main["erreurs"]);$i++){
            echo "<li>".$main["erreurs"][$i]."</li>\n";
        }//para
    ?>
</ul>
<a href="<?php echo $main["href"] ?>"><?php echo $main["lien"] ?></a>

Apresenta a lista de erros definida em $main['erreurs']. Além disso, pode sugerir um link de retorno, geralmente para a página que antecedeu a página de erros. Este link será definido por um texto $main['lien'] e um URL $main['href']. Para que este link não apareça, basta inserir uma cadeia vazia em $main['lien']. Aqui está um exemplo de página de erros no caso de o utilizador se identificar incorretamente:

Image

7.5.7. A página de informações

Por vezes, poderá ser necessário fornecer ao utilizador uma simples informação, por exemplo, que a sua autenticação foi bem-sucedida. Para tal, utilizar-se-á a seguinte página infos.php:

<?php echo $main["infos"] ?>

Para apresentar uma informação em resposta a um pedido de um cliente,

  • colocamos a informação em $main['infos']
  • colocaremos o URL de infos.php em $main['contenu']

Eis, por exemplo, a informação devolvida quando o utilizador se identificou corretamente:

Image

7.6. O funcionamento da aplicação

Temos agora uma boa ideia da estrutura geral da aplicação a ser desenvolvida. Resta-nos apresentar os percursos do utilizador na aplicação, as ações que este pode realizar e as respostas que recebe do servidor. Feito isto, poderemos escrever as funções que processam os diferentes pedidos de um cliente. A seguir, iremos apresentar o funcionamento da aplicação através das páginas apresentadas ao utilizador em resposta a algumas dessas ações. Em cada caso, especificaremos os seguintes pontos:

action utilisateur
ação inicial do utilizador que levou à resposta apresentada
paramètres envoyés
os parâmetros enviados pelo navegador do cliente ao servidor em resposta à ação manual do utilizador
page réponse
o script que gera a zona 4 da página modelo

7.6.1. A autenticação

Antes de poder utilizar a aplicação, o utilizador terá de se identificar através da seguinte página:

Image

action utilisateur
1 - pedido inicial do URL apparticles.php
2 - utilização da opção «Autenticação» do menu
3 - solicitação direta do URL articles.php com parâmetros incorretos
paramètres envoyés
1 - sem parâmetros
2 - action=autenticar?fase=0
3 - uma lista de parâmetros incorretos
page réponse
login.php

Na página inicial, o link [Ajouter un article] tem o seguinte formato: action=addarticle?phase=0. Os outros links têm o mesmo formato, com action=(autenticar, atualizarartigo, eliminarartigo, selecionarartigo, sql). O utilizador preenche o formulário e utiliza o botão [Connexion]:

Image

A resposta é a seguinte:

Image

action utilisateur
botão [Connexion]
paramètres envoyés
action=autenticar?fase=1
page réponse
infos.php

O título da página foi alterado para indicar o nome de utilizador e os seus direitos de administrador/utilizador. Além disso, todas as ligações da zona 2 foram alteradas para refletir o facto de ter sido iniciada uma sessão. Foi-lhes adicionado o parâmetro PHPSESSID=[PHPSESSID].

Se o servidor não tiver conseguido identificar o cliente, este receberá uma resposta diferente:

Image

action utilisateur
botão [Connexion]
paramètres envoyés
action=autenticar?fase=1
page réponse
erreurs.php

O link [Retour à la page de login] é um link para o URL apparticles.php?action=authentifier&phase=2&txtLogin=x. Este link redireciona o cliente para a página de início de sessão, onde o campo de início de sessão é preenchido com o valor do parâmetro txtLogin:

Image

action utilisateur
ligação [Retour à la page de login]
paramètres envoyés
action=autenticar?fase=2&txtLogin=x
page réponse
login.php

7.6.2. Adicionar um artigo

O link do menu [Ajouter un article] direciona para a seguinte página na zona 4 da página-modelo:

Image

action utilisateur
ligação [Ajouter un article]
paramètres envoyés
action=addArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
addarticle.php

O utilizador preenche os campos e envia tudo para o servidor através do botão [Ajouter], que é do tipo submit. Não é efetuada qualquer verificação do lado do cliente. É o servidor que a realiza. O servidor pode enviar, em resposta, uma página de erros, como no exemplo abaixo:

Pedido
Resposta
action utilisateur
botão [Ajouter]
paramètres envoyés
action=addArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
erreurs.php

O link [Retour à la page d'ajout d'article] permite regressar à página de introdução de dados:

Pedido
Resposta
action utilisateur
ligação [Retour à la page d'ajout d'article]
paramètres envoyés
action=addArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
article.php

Se a adição for efetuada sem erros, o utilizador recebe uma mensagem de confirmação:

Pedido
Resposta
action utilisateur
botão [Ajouter]
paramètres envoyés
action=addArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.3. Consulta de artigos

O link do menu [Lister des articles] direciona para a seguinte página na zona 4 da página-modelo:

Image

action utilisateur
ligação do menu [Lister des articles]
paramètres envoyés
action=selectArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
select1.php

Será emitida uma consulta select [colonnes] from articles where [where] orderby [orderby] na tabela de artigos, em que [colonnes], [where] e [orderby] são os valores dos campos acima. Por exemplo:

Pedido
Resposta
action utilisateur
botão [Afficher]
paramètres envoyés
action=selectArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
select2.php

A solicitação pode estar incorreta; nesse caso, o cliente recebe uma página de erros:

Pedido
Resposta

Em ambos os casos (com ou sem erros), a ligação [Retour à la page de sélection d'articles] permite regressar à página select1.php:

Pedido
Resposta
action utilisateur
ligação [Retour à la page de sélection d'articles]
paramètres envoyés
action=selectArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
select1.php

7.6.4. Alteração de artigos

O link do menu [Modifier un article] leva à seguinte página na zona 4 da página-modelo:

Image

action utilisateur
ligação do menu [Modifier un article]
paramètres envoyés
action=updateArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
updatearticle1.php

Seleciona-se o código do artigo a modificar na lista suspensa e executa-se [OK] para modificar o artigo com esse código:

Pedido
Resposta
action utilisateur
botão [OK]
paramètres envoyés
action=updateArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
updatearticle2.php

Depois de aceder à ficha do artigo a modificar, o utilizador pode efetuar as suas alterações:

Pedido
Resposta
action utilisateur
botão [Modifier]
paramètres envoyés
action=updateArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
infos.php

O utilizador pode cometer erros durante a edição:

Pedido
Resposta

A ligação [Retour à la page de modification d'article] permite regressar à página de introdução de dados:

Image

action utilisateur
ligação [Retour à la page de modification d'article]
paramètres envoyés
action=updateArticle?phase=3&PHPSESSID=[PHPSESSID]
page réponse
updatearticle2.php

7.6.5. Eliminação de um artigo

O link do menu [Supprimer un article] direciona para a seguinte página na zona 4 da página-modelo:

Image

action utilisateur
ligação do menu [Supprimer un article]
paramètres envoyés
action=deleteArticle?phase=0&PHPSESSID=[PHPSESSID]
page réponse
deletearticle1.php

O utilizador seleciona o código do artigo a eliminar numa lista suspensa:

Pedido
Resposta
action utilisateur
botão [OK]
paramètres envoyés
action=deleteArticle?phase=1&PHPSESSID=[PHPSESSID]
page réponse
deletearticle2.php

O utilizador confirma a eliminação do artigo com o botão [Supprimer]:

Pedido
Resposta
action utilisateur
botão [Supprimer]
paramètres envoyés
action=deleteArticle?phase=2&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.6. Envio de pedidos de administrador

O link do menu [Requête SQL] direciona para a seguinte página na zona 4 da página-modelo:

Image

action utilisateur
ligação do menu [Requête SQL]
paramètres envoyés
action=sql?phase=0&PHPSESSID=[PHPSESSID]
page réponse
sql1.php

Digita-se o texto da consulta SQL no campo de introdução e utiliza-se o botão [Exécuter] para a executar. Apenas um administrador pode emitir estas consultas, como mostra o exemplo seguinte:

Pedido
Resposta
action utilisateur
botão [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
erreurs.php

A ligação [Retour à la page d'émission de requêtes SQL] permite regressar à página de introdução de dados:

Image

action utilisateur
ligação [Retour à la page d'émission de requêtes SQL]
paramètres envoyés
action=sql?phase=2&PHPSESSID=[PHPSESSID]
page réponse
sql1.php

Se for administrador e a consulta estiver sintaticamente correta:

Pedido

obtém-se o resultado da consulta:

Resposta
action utilisateur
botão [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
sql2.php

É possível emitir consultas para atualizar as tabelas:

Pedido
Resposta
action utilisateur
botão [Exécuter]
paramètres envoyés
action=sql?phase=1&PHPSESSID=[PHPSESSID]
page réponse
infos.php

7.6.7. Trabalho a realizar

Escrever os scripts e funções necessários para a aplicação:

identificador
tipo
função
apparticles.php
script
o ponto de entrada do processamento dos pedidos dos clientes
authentifier_0
função
processa o pedido com os parâmetros action=autenticar&fase=0
authentifier_1
função
processa a solicitação com os parâmetros action=autenticar&fase=1
authentifier_2
função
processa a solicitação com os parâmetros action=autenticar&fase=2
addarticle_0
função
processa o pedido com os parâmetros action=addArticle&phase=0
addarticle_1
função
processa o pedido com os parâmetros action=addArticle&phase=1
addarticle_2
função
processa o pedido com os parâmetros action=addArticle&phase=2
updatearticle_0
função
processa a solicitação configurada com action=updatearticle&phase=0
updatearticle_1
função
processa a solicitação com os parâmetros action=updatearticle&phase=1
updatearticle_2
função
processa a solicitação com os parâmetros action=updatearticle&phase=2
updatearticle_3
função
processa a solicitação com os parâmetros action=updatearticle&phase=3
deletearticle_0
função
processa a solicitação com os parâmetros action=deletearticle&phase=0
deletearticle_1
função
processa o pedido com os parâmetros action=deletearticle&phase=1
deletearticle_2
função
processa a solicitação com os parâmetros action=deletearticle&phase=2
selectarticle_0
função
processa a solicitação com os parâmetros action=selectarticle&phase=0
selectarticle_1
função
processa a solicitação com os parâmetros action=selectarticle&phase=1
selectarticle_2
função
processa a solicitação com os parâmetros action=selectarticle&phase=2
sql_0
função
processa a solicitação com os parâmetros action=sql&phase=0
sql_1
função
processa a solicitação com os parâmetros action=sql&phase=1
sql_2
função
processa o pedido com os parâmetros action=sql&phase=2
main.php
script
gera a página tipo
login.php
script
gera a página de início de sessão
erreurs.php
script
gera a página de erros
infos.php
script
gera a página de informações
addarticle.php
script
gera a página de adição de um artigo
updatearticle1.php
script
gera a página 1 da edição de um artigo
updatearticle2.php
script
gera a página 2 da edição de um artigo
deletearticle1.php
script
gera a página 1 da eliminação de um artigo
deletearticle2.php
script
gera a página 2 da eliminação de um artigo
select1.php
script
gera a página 1 da seleção de artigos
select2.php
script
gera a página 2 da seleção de artigos
sql1.php
script
gera a página 1 do envio de pedidos
sql2.php
script
gera a página 2 da emissão de pedidos

7.7. Desenvolver a aplicação

Neste momento, temos uma aplicação que cumpre a sua função com uma usabilidade aceitável. Vamos aperfeiçoá-la em vários aspetos:

  • o SGBD
  • a sua segurança
  • o seu aspeto
  • o seu desempenho

7.7.1. Alterar o tipo de base de dados

O nosso estudo partia do princípio de que o SGBD utilizado era o MySQL. Altere para SGBD e demonstre que a única alteração a efetuar é na definição da variável $dDSN no ficheiro de configuração config.php.

7.7.2. Melhorar a segurança

Ao desenvolver uma aplicação web, nunca se deve partir do princípio de que o cliente é um navegador e que o pedido que nos envia é controlado pelo formulário que lhe foi enviado antes desse pedido. Qualquer programa pode ser cliente de uma aplicação web e, por isso, enviar qualquer pedido, com ou sem parâmetros, à aplicação. Por isso, esta deve verificar tudo.

Se analisarmos o código do script apparticles.php, verificamos

  • que nenhuma ação além da autenticação pode ocorrer sem uma sessão. Esta só existe se o utilizador tiver conseguido autenticar-se. Recorde-se que uma sessão é identificada por uma cadeia de caracteres bastante longa, denominada «token de sessão», que tem o seguinte formato: 176a43609572907333118333edf6d1fb. Este token pode ser enviado para a aplicação de várias formas, por exemplo, utilizando um URL configurado:

apparticles.php?PHPSESSID=176a43609572907333118333edf6d1fb. 

Um programa que solicitasse repetidamente o URL anterior, variando o token aleatoriamente na esperança de encontrar o token correto, provavelmente demoraria vários dias a gerar a combinação certa, dado o grande número de combinações possíveis. Nessa altura, como a sessão tem uma duração limitada, provavelmente já terá terminado. Outro risco seria o token, ao ser transmitido em texto simples pela rede, ser interceptado. O risco é real. Nesse caso, pode-se utilizar uma ligação encriptada entre o servidor e o seu cliente.

  • de modo a que, uma vez iniciada a sessão, apenas determinadas ações sejam autorizadas. Um URL configurado com action=tricher&phase=0&PHPSESSID=[PHPSESSID] seria rejeitado, uma vez que a ação «tricher» não é uma ação autorizada. Quando os parâmetros (ação, fase) não são reconhecidos, a nossa aplicação responde com a página de autenticação.

No entanto, a aplicação não verifica se as ações autorizadas se sucedem corretamente. Por exemplo, as duas ações seguintes:

  1. action=addArticle&phase=0&PHPSESSID=[PHPSESSID]
  2. action=updateArticle&phase=1&PHPSESSID=[PHPSESSID]

são duas ações autorizadas. No entanto, a ação 2 não está autorizada a seguir a ação 1.

Como acompanhar a sequência de URL solicitadas pelo navegador do cliente?

Podemos recorrer a duas variáveis PHP: $_SERVER['REQUEST_URI] e $_SERVER['HTTP_REFERER], que são duas informações enviadas pelos navegadores dos clientes nos seus cabeçalhos HTTP.

$_SERVER['REQUEST_URI]: Trata-se do URI solicitado pelo cliente. Por exemplo

/apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

$_SERVER['HTTP_REFERER]: Este é o URL que estava a ser visualizado no navegador antes do novo URL que o navegador está a solicitar (o URI anterior). Por exemplo, se o navegador que visualizou o URI mencionado anteriormente fizer um novo pedido a um servidor, a variável $_SERVER['HTTP_REFERER'] desse servidor terá como valor

http://máquina:porta//apparticles.php?action=addArticle&phase=0&PHPSESSID=[PHPSESSID]

Para verificar se duas ações da nossa aplicação se sucedem na ordem correta, pode-se proceder da seguinte forma:

Durante a ação 1:

  • regista-se o URI solicitado (URI1) e anota-se na sessão

Na ação 2:

  • recupera-se o HTTP-REFERER da ação 2. A partir daí, deduz-se o URI (URI2) a partir do URL que estava anteriormente visualizado no navegador que efetuou o pedido.
  • Recupera-se o URI (URI1), que estava armazenado na sessão e que corresponde ao URI da ação solicitada anteriormente ao servidor
  • Se a ação 2 se seguir à ação 1, então deve verificar-se que URI2 = URI1. Caso contrário, recusar-se-ia a execução da ação solicitada e apresentar-se-ia a página de autenticação.
  • Regista-se na sessão o URI URI2 da ação em curso para verificação da ação seguinte. E assim sucessivamente.

Eis um exemplo. Após a autenticação, seleciona-se o link [Ajouter un article]:

Image

O URL desta página é:

http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=addArticle&phase=0&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

Diretamente no campo [Adresse] do navegador, alteramos o URL da seguinte forma:

http://localhost:81/st/php/articles/gestion/articles8/apparticles.php?action=deleteArticle&phase=1&PHPSESSID=006a63e6027f16c70b63cdae93405eeb

Obtenemos então a página de autenticação:

Image

Isto merece uma explicação. Quando solicitamos um URL digitando diretamente a nossa identidade no campo de endereço do navegador, este não envia o cabeçalho HTTP_REFERER. A nossa aplicação não encontra, portanto, o URI da ação anterior, URI, que tinha memorizado na sessão. Assim, devolve a página de autenticação como resposta.

Este mecanismo é eficaz para os navegadores, mas não para um cliente programado. Este pode enviar o cabeçalho HTTP_REFERER que quiser. Pode, portanto, «fazer batota», alegando que passou por determinada etapa, quando na verdade não o fez. É, portanto, necessário garantir que a sequência das etapas é respeitada. Assim, se a ação solicitada for action=addArticle&phase=1 (introdução de dados), então a ação anterior deve ser necessariamente action=deleteArticle&phase=0 (pedido inicial da página de introdução de dados) ou action=addArticle&phase=2 (regresso à introdução de dados após adição incorreta). Da mesma forma, se a ação solicitada for action=addArticle&phase=2 (adição), então a ação anterior deve ser action=addArticle&phase=1 (introdução de dados). É possível obrigar o utilizador a respeitar estas sequências.

Enquanto o primeiro mecanismo é geral e pode aplicar-se a qualquer aplicação, o segundo requer uma codificação específica para cada aplicação e é mais complexo: é necessário analisar todas as ações possíveis do utilizador e as suas sequências. Estas últimas podem ser armazenadas num dicionário, como mostra o código seguinte:

  // autenticação
  $dPrec['authentifier']['0']=array();
  $dPrec['authentifier']['1']=array(
         array('action'=>'authentifier','phase'=>'0'),
    array('action'=>'authentifier','phase'=>'2')
  );
  $dPrec['authentifier']['2']=array(
         array('action'=>'authentifier','phase'=>'1'),
  );

   // adição de artigo
  $dPrec['addarticle']['0']=array();  
  $dPrec['addarticle']['1']=array(
         array('action'=>'addarticle','phase'=>'0'),
    array('action'=>'addarticle','phase'=>'2')
  );
  $dPrec['addarticle']['2']=array(
         array('action'=>'addarticle','phase'=>'1'),
  );

   // alteração de artigo
  $dPrec['updatearticle']['0']=array();  
  $dPrec['updatearticle']['1']=array(
         array('action'=>'updatearticle','phase'=>'0'),
  );
  $dPrec['updatearticle']['2']=array(
         array('action'=>'updatearticle','phase'=>'1'),
    array('action'=>'updatearticle','phase'=>'3')
  );
  $dPrec['updatearticle']['3']=array(
         array('action'=>'updatearticle','phase'=>'2'),
  );

   // eliminação de artigo
  $dPrec['deletearticle']['0']=array();  
  $dPrec['deletearticle']['1']=array(
         array('action'=>'deletearticle','phase'=>'0'),
  );
  $dPrec['deletearticle']['2']=array(
         array('action'=>'deletearticle','phase'=>'1'),
  );

      // seleção de artigos
  $dPrec['selectarticle']['0']=array();  
  $dPrec['selectarticle']['1']=array(
         array('action'=>'selectarticle','phase'=>'0'),
    array('action'=>'selectarticle','phase'=>'2')
  );
  $dPrec['selectarticle']['2']=array(
         array('action'=>'selectarticle','phase'=>'1'),
  );

      // pedido de administrador
  $dPrec['sql']['0']=array();  
  $dPrec['sql']['1']=array(
         array('action'=>'sql','phase'=>'0'),
    array('action'=>'sql','phase'=>'2')
  );
  $dPrec['sql']['2']=array(
         array('action'=>'sql','phase'=>'1'),
  );

$dPrec['action']['phase'] é uma tabela que contém as ações que podem preceder a ação e a fase que servem de índice para o dicionário. Estas ações precedentes são também representadas por um dicionário com duas chaves: «ação» e «fase». Se uma ação puder ser precedida por qualquer ação, então $dPrec['action']['phase'] será uma tabela vazia. A ausência de uma ação no dicionário significa que esta não está autorizada. Consideremos a ação «autenticar» acima referida:

  // autenticação
  $dPrec['authentifier']['0']=array();
  $dPrec['authentifier']['1']=array(
         array('action'=>'authentifier','phase'=>'0'),
    array('action'=>'authentifier','phase'=>'2')
  );
  $dPrec['authentifier']['2']=array(
         array('action'=>'authentifier','phase'=>'1'),
  );

O código acima significa que a ação action=authentifier&phase=0 pode ser precedida por qualquer ação, que a ação action=authentifier&phase=1 pode ser precedida pela ação action=authentifier&phase=0 ou pela ação action=authentifier&phase=2 e que a ação action=authentifier&phase=2 pode ser precedida pela ação action=authentifier&phase=1.

Escreva a seguinte função:

  // ---------------------------------------------------------------
  function enchainementOK(&$dConfig, &$dSession, $sAction, $sPhase){
       // verifica se a ação em curso ($sAction, $sPhase) pode seguir a ação anterior
         // armazenada em $dSession['précédent']
         // o dicionário de sequências autorizadas encontra-se em $dConfig['précédents']
         // retorna TRUE se a sequência for possível, FALSE caso contrário
....

Esta função permite que a aplicação principal verifique se a sequência de ações está correta:

<?php
     // gestão de uma tabela de artigos
  include "config.php";
  include "articles.php";  

  // sessão
  session_start();
  $dSession=$_SESSION["session"];

   // ação a realizar
  $sAction=$_POST["action"] ? $_POST["action"] : $_GET["action"] ? $_GET["action"] : "authentifier";
  $sAction=strtolower($sAction);
   // fase eventual da ação
  $sPhase=$_POST["phase"] ? $_POST["phase"] : $_GET["phase"] ? $_GET["phase"] : "0";

     // existe alguma sessão em curso?  
  if(! isset($dSession)){
      // autenticação do utilizador
    if($sAction=="authentifier" && $sPhase=="0") authentifier_0($dConfig);
    if($sAction=="authentifier" && $sPhase=="1") authentifier_1($dConfig);   
    if($sAction=="authentifier" && $sPhase=="2") authentifier_2($dConfig);    
     // ação anómala
    authentifier_0($dConfig);        
  }//if - sem sessão

     // recuperar a sessão
  $dSession=unserialize($dSession);

     // A sequência de ações é normal?
  if( ! enchainementOK($dConfig,$dSession,$sAction,$sPhase)){
     // sequência anormal
    authentifier_0($dConfig);        
  }//if

     // processamento das ações
  if($sAction=="authentifier"){
   if($sPhase=="0") authentifier_0($dConfig);  
   if($sPhase=="1") authentifier_1($dConfig);  
   if($sPhase=="2") authentifier_2($dConfig);  
  }//if     
  if($sAction=="addarticle"){
...

7.7.3. Evoluir o «aspecto»

Lembremo-nos de que uma das condições estabelecidas durante o estudo desta aplicação era que esta tivesse de ser evolutiva. Suponhamos que, ao fim de algumas semanas, se verifique que a ergonomia da aplicação precisa de ser melhorada. Altere a aplicação de forma a que a estrutura e a apresentação da página-modelo sejam alteradas. As alterações serão efetuadas em dois locais:

  • no script main.php, que define a estrutura da página-modelo. Atualize-a.
  • na folha de estilo que define o «aspecto» da aplicação. Altere-a.

7.7.4. Melhorar o desempenho

Por enquanto, optámos por um navegador cliente leve: este limita-se apenas à apresentação. É possível fazer com que ele execute processamentos, incluindo scripts nas páginas Web que lhe enviamos. Estes podem estar em diferentes linguagens, nomeadamente VBScript e JavaScript. O Internet Explorer e o Netscape dominam o mercado dos navegadores numa proporção próxima de 60/40. Além disso, o IE existe apenas no ambiente Windows e não no Unix, por exemplo, onde é o Netscape que predomina. O Netscape não executa nativamente scripts VBScript, ao passo que ambos os navegadores executam scripts JavaScript. Como o Netscape ainda ocupa uma quota significativa do mercado dos navegadores, os scripts VBScript devem ser evitados. Por isso, é o JavaScript que é geralmente utilizado nos scripts do lado do cliente.

São delegadas aos scripts do lado do cliente as operações em que o servidor não precisa de intervir. Na nossa aplicação, seria interessante que o navegador do cliente só enviasse um pedido ao servidor depois de o ter verificado. Assim, é desnecessário enviar ao servidor um pedido de autenticação quando o utilizador deixou o campo [login] em branco no formulário de autenticação. É preferível avisar o utilizador de que o seu pedido está incorreto:

Image

Note-se que isto não impedirá o servidor de verificar se o campo de login está preenchido, uma vez que o seu cliente não é necessariamente um navegador e, por isso, a verificação anterior pode não ter sido efetuada. Partir do princípio de que o cliente é um navegador constitui um risco significativo para a segurança da aplicação.

Identifique os diferentes momentos em que o navegador envia informações para o servidor e, sempre que estas puderem ser verificadas, escreva uma ou mais funções JavaScript que permitam ao navegador verificar a validade das informações antes do seu envio para o servidor.

Retomando o exemplo anterior, o script login.php que gera a página de autenticação passa a ser o seguinte:


<script language="javascript">
    function check(){
       // verifica-se se existe efetivamente um login
    with(document.frmLogin){
        champs=/^\s*$/.exec(txtLogin.value);
      if(champs!=null){
          // sem início de sessão
        alert("Vous n'avez pas indiqué de login");
        txtLogin.focus();
        return;
      }//if
       // os dados estão presentes — enviam-se para o servidor
      submit();
    }//com
  }//verificação
</script>   
    
<form name="frmLogin" method="post" action="<?php echo $main["post"] ?>">
    <table>
        <tr>
            <td>login</td>
            <td><input type="text" value="<?php echo $main["login"] ?>" name="txtLogin" class="text"></td>
        </tr>
        <tr>
            <td>mot de passe</td>
            <td><input type="password" value="" name="txtMdp" class="text"></td>
      <td><input type="button" onclick="check()" value="Connexion" class="submit"></td>      
        </tr>
    </table>
</form>    

7.8. Para aprofundar

Para concluir, apresentamos algumas sugestões para aprofundar este estudo de caso:

  • seria interessante verificar se a página-modelo desta aplicação poderia ser transformada numa classe. Esta poderia então ser utilizada noutras aplicações.
  • A nossa aplicação está bem adaptada a clientes do tipo navegador, mas menos a clientes do tipo «Aplicação autónoma». Estes últimos têm de:
    • criar uma ligação TCP com o servidor
    • «comunicar» com ele através de HTTP
    • analisar as suas respostas HTML para encontrar a informação desejada, uma vez que o cliente autónomo provavelmente não estará interessado no código de apresentação HTML destinado aos navegadores.

Seria interessante que a nossa aplicação gerasse XML em vez de HTML. Os seus clientes poderiam então ser, indistintamente, navegadores (bastante recentes, no entanto) ou aplicações autónomas. Estas últimas não teriam qualquer dificuldade em encontrar a informação que procuram, uma vez que a resposta XML do servidor não conteria qualquer informação de apresentação, apenas conteúdo.

  • Seria certamente necessário ter em conta os acessos simultâneos à base de artigos. Há, pelo menos, dois pontos a esclarecer:
  1. o SGBD utilizado pela aplicação gere corretamente o acesso simultâneo a um mesmo artigo? Por exemplo, o que acontece se dois utilizadores alterarem o mesmo artigo ao mesmo tempo (carregando no botão [Modifier] em simultâneo)? Isso depende provavelmente do SGBD subjacente.
  2. Atualmente, a nossa aplicação não suporta acessos simultâneos. No entanto, a base de dados deverá permanecer num estado coerente, embora sejam de esperar algumas surpresas. Consideremos a seguinte sequência de eventos:
      • o utilizador U1 acede à edição de um artigo
      • o utilizador U2 inicia a eliminação do mesmo artigo pouco depois
      • cada uma das duas ações requer trocas cliente-servidor. Dependendo da forma de trabalhar de cada um, o utilizador U2 pode concluir o seu trabalho antes de U1. Quando este último terminar as suas alterações e as validar através de [Modifier], receberá a página de informações como resposta, com o SGBD a indicar-lhe que [0 ligne(s) ont été modifiées], isto porque a página que pretendia alterar foi eliminada entretanto. O utilizador ficará, sem dúvida, surpreendido. Do ponto de vista da ergonomia, seria certamente preferível apresentar uma página que indicasse melhor o erro. Além disso, poderia considerar-se oferecer ao utilizador acesso exclusivo a um artigo assim que este iniciasse a sua atualização. Um outro utilizador que pretendesse atualizar o mesmo artigo receberia a resposta de que outra atualização está em curso. Isto colocará um problema se o primeiro utilizador demorar a validar a sua atualização: os outros ficarão bloqueados. Há aqui soluções a encontrar que dependerão em grande medida das capacidades do SGBD utilizado. A Oracle, por exemplo, tem mais capacidades nesta área do que o MySQL.