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:

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:
![]() |
|
O conteúdo da tabela poderia ser o seguinte:

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:
![]() |
|
O conteúdo da tabela poderia ser o seguinte:

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:
![]() |
|
O seu conteúdo, utilizado inicialmente como teste, poderia ser o seguinte:

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
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

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:
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:
indica a ação em curso entre as seguintes:
| |||||||||||
uma ação pode ser realizada em várias etapas — indica a etapa em curso | |||||||||||
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:
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
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:
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> | |
<H1> (Cabeçalho 1) | |
<A> (Âncora) | |
define os atributos de apresentação da âncora quando o utilizador passa o cursor por cima dela | |
<FIELDSET> - esta baliza não é reconhecida por todos os navegadores | |
<LEGEND> - esta baliza não é reconhecida por todos os navegadores | |
<INPUT> | |
<INPUT class="TEXT"> | |
<INPUT class="SUBMIT"> | |
<TH> (Cabeçalho da tabela) | |
<TD class="menutitle"> (Dados da tabela) | |
<TD class="menublock"> | |
<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:
- 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).
- 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
- 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:

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:

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:
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:

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:
ação inicial do utilizador que levou à resposta apresentada | |
os parâmetros enviados pelo navegador do cliente ao servidor em resposta à ação manual do utilizador | |
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:

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 | |
1 - sem parâmetros 2 - action=autenticar?fase=0 3 - uma lista de parâmetros incorretos | |
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]:

A resposta é a seguinte:

botão [Connexion] | |
action=autenticar?fase=1 | |
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:

botão [Connexion] | |
action=autenticar?fase=1 | |
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:

ligação [Retour à la page de login] | |
action=autenticar?fase=2&txtLogin=x | |
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:

ligação [Ajouter un article] | |
action=addArticle?phase=0&PHPSESSID=[PHPSESSID] | |
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 |
![]() | ![]() |
botão [Ajouter] | |
action=addArticle?phase=1&PHPSESSID=[PHPSESSID] | |
erreurs.php |
O link [Retour à la page d'ajout d'article] permite regressar à página de introdução de dados:
Pedido | Resposta |
![]() | ![]() |
ligação [Retour à la page d'ajout d'article] | |
action=addArticle?phase=2&PHPSESSID=[PHPSESSID] | |
article.php |
Se a adição for efetuada sem erros, o utilizador recebe uma mensagem de confirmação:
Pedido | Resposta |
![]() | ![]() |
botão [Ajouter] | |
action=addArticle?phase=1&PHPSESSID=[PHPSESSID] | |
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:

ligação do menu [Lister des articles] | |
action=selectArticle?phase=0&PHPSESSID=[PHPSESSID] | |
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 |
![]() |
botão [Afficher] | |
action=selectArticle?phase=1&PHPSESSID=[PHPSESSID] | |
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 |
![]() |
ligação [Retour à la page de sélection d'articles] | |
action=selectArticle?phase=2&PHPSESSID=[PHPSESSID] | |
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:

ligação do menu [Modifier un article] | |
action=updateArticle?phase=0&PHPSESSID=[PHPSESSID] | |
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 |
![]() | ![]() |
botão [OK] | |
action=updateArticle?phase=1&PHPSESSID=[PHPSESSID] | |
updatearticle2.php |
Depois de aceder à ficha do artigo a modificar, o utilizador pode efetuar as suas alterações:
Pedido | Resposta |
![]() | ![]() |
botão [Modifier] | |
action=updateArticle?phase=2&PHPSESSID=[PHPSESSID] | |
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:

ligação [Retour à la page de modification d'article] | |
action=updateArticle?phase=3&PHPSESSID=[PHPSESSID] | |
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:

ligação do menu [Supprimer un article] | |
action=deleteArticle?phase=0&PHPSESSID=[PHPSESSID] | |
deletearticle1.php |
O utilizador seleciona o código do artigo a eliminar numa lista suspensa:
Pedido | Resposta |
![]() | ![]() |
botão [OK] | |
action=deleteArticle?phase=1&PHPSESSID=[PHPSESSID] | |
deletearticle2.php |
O utilizador confirma a eliminação do artigo com o botão [Supprimer]:
Pedido | Resposta |
![]() | ![]() |
botão [Supprimer] | |
action=deleteArticle?phase=2&PHPSESSID=[PHPSESSID] | |
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:

ligação do menu [Requête SQL] | |
action=sql?phase=0&PHPSESSID=[PHPSESSID] | |
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 |
![]() | ![]() |
botão [Exécuter] | |
action=sql?phase=1&PHPSESSID=[PHPSESSID] | |
erreurs.php |
A ligação [Retour à la page d'émission de requêtes SQL] permite regressar à página de introdução de dados:

ligação [Retour à la page d'émission de requêtes SQL] | |
action=sql?phase=2&PHPSESSID=[PHPSESSID] | |
sql1.php |
Se for administrador e a consulta estiver sintaticamente correta:
Pedido |
![]() |
obtém-se o resultado da consulta:
Resposta |
![]() |
botão [Exécuter] | |
action=sql?phase=1&PHPSESSID=[PHPSESSID] | |
sql2.php |
É possível emitir consultas para atualizar as tabelas:
Pedido |
![]() |
Resposta |
![]() |
botão [Exécuter] | |
action=sql?phase=1&PHPSESSID=[PHPSESSID] | |
infos.php |
7.6.7. Trabalho a realizar
Escrever os scripts e funções necessários para a aplicação:
identificador | tipo | função |
script | o ponto de entrada do processamento dos pedidos dos clientes | |
função | processa o pedido com os parâmetros action=autenticar&fase=0 | |
função | processa a solicitação com os parâmetros action=autenticar&fase=1 | |
função | processa a solicitação com os parâmetros action=autenticar&fase=2 | |
função | processa o pedido com os parâmetros action=addArticle&phase=0 | |
função | processa o pedido com os parâmetros action=addArticle&phase=1 | |
função | processa o pedido com os parâmetros action=addArticle&phase=2 | |
função | processa a solicitação configurada com action=updatearticle&phase=0 | |
função | processa a solicitação com os parâmetros action=updatearticle&phase=1 | |
função | processa a solicitação com os parâmetros action=updatearticle&phase=2 | |
função | processa a solicitação com os parâmetros action=updatearticle&phase=3 | |
função | processa a solicitação com os parâmetros action=deletearticle&phase=0 | |
função | processa o pedido com os parâmetros action=deletearticle&phase=1 | |
função | processa a solicitação com os parâmetros action=deletearticle&phase=2 | |
função | processa a solicitação com os parâmetros action=selectarticle&phase=0 | |
função | processa a solicitação com os parâmetros action=selectarticle&phase=1 | |
função | processa a solicitação com os parâmetros action=selectarticle&phase=2 | |
função | processa a solicitação com os parâmetros action=sql&phase=0 | |
função | processa a solicitação com os parâmetros action=sql&phase=1 | |
função | processa o pedido com os parâmetros action=sql&phase=2 | |
script | gera a página tipo | |
script | gera a página de início de sessão | |
script | gera a página de erros | |
script | gera a página de informações | |
script | gera a página de adição de um artigo | |
script | gera a página 1 da edição de um artigo | |
script | gera a página 2 da edição de um artigo | |
script | gera a página 1 da eliminação de um artigo | |
script | gera a página 2 da eliminação de um artigo | |
script | gera a página 1 da seleção de artigos | |
script | gera a página 2 da seleção de artigos | |
script | gera a página 1 do envio de pedidos | |
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:
- action=addArticle&phase=0&PHPSESSID=[PHPSESSID]
- 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
$_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
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]:

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:

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:

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:
- 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.
- 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.































