Desenvolvimento de aplicações Web

PHP e MySQL

Tutorial onde se descreve a implementação de uma pequena aplicação Web para gestão de logs e se usam os vários serviços desta aplicação para demonstrar a utilização da nova classe PHP para trabalhar com bases de dados MySQL: PDO.


@Copyright José Carlos Ramalho - DI/UM - Nov. 2012
Código base dos exemplos fornecido pelo aluno António Oliveira da Silva (pg22820)

Pretende-se desenvolver uma pequena aplicação Web para gerir os logs de uma qualquer outra aplicação de grande dimensão. No tutorial de PHP e XML havíamos colocado a persistência dos dados num documento XML com a estrutura que é apresentada mais abaixo, neste tutorial iremos colocal a persistência numa base de dados MySQL mas manteremos também a informação no documento XML.

Assim, o Log Manager terá 5 serviços:

Reset:
limpa o registo de logs;
Add Log:
acrescenta um registo de log;
List Logs:
lista os logs registados numa tabela HTML;
Load from XML:
Carrega os registos presentes no documento XML na base de dados;
Save to XML:
Faz um dump da base de dados para o documento XML.

No tutorial anterior especificou-se um XML Schema para documentos XML que armazenam logs que se mostra na figura seguinte.

Schema do logbook

Neste tutorial vamos criar uma base de dados MySQL para a mesma informação.

Como o objetivo é obter uma aplicação simples para exemplificar a utilização da classe PDO, a base de dados é constituída apenas por uma tabela onde se guardam os registos dos logs.

Estrutura da base de dados

A classe PDO define uma interface leve e consistente para aceder a bses de dados em PHP. Esta classe permite criar drivers específicos para bases de dados específicas através do mecanismo de extensão de classes.

A classe PDO fornece uma camada de abstração de acesso aos dados, o que significa que, independentemente de qual a base de dados a que se está a aceder, usam-se as mesmas funções para realizar inserções, consultas ou pesquisas de dados.

Esta classe usa algumas funcionalidades OO implementadas na nova versão do PHP e apenas funciona em versões do PHP superiores à 5.1. Neste momento as recomendações são para que se use esta classe em detrimento dos métodos procedimentais nativos nas versões anteriores do PHP.

Conexão à base de dados

É estabelecida uma ligação à base de dados quando se cria uma instância da classe. A classe PDO trata de abstrair o driver específico para o sistema de gestão de bases de dados em causa. O construtor da classe aceita todos os parâmetros necessários ao estabelecimento da ligação a uma base de dados específica com autenticação. A seguir apresenta-se um exemplo de ligação à nossa base de dados instalada localmente.

<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
?>

No exemplo apresentado, $user e $pass correspondem ao nome e password de um utilizador com permissões para realizar operações sobre a base de dados.

Erros de conexão à base de dados

Se pretendermos tratar os erros ocorridos na tentativa de estabelecer uma ligação podemos apanhar a excepção gerada.

<?php
try {
    $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
    foreach($dbh->query('SELECT * from FOO') as $row) {
        print_r($row);
    }
    $dbh = null;
} catch (PDOException $e) {
    print "Error!: " . $e->getMessage() . "
"; die(); } ?>

No exemplo apresentado, $e é a excepção gerada quando não se consegue realizar a conexão.

A nossa conexão à base de dados

Como no caso da nossa aplicação os parâmetros de ligação são sempre os mesmos, pode-se criar uma extensão à classe reescrevendo o construtor com aqueles parâmetros pré-estabelecidos.

<?php
	class myDB extends PDO{
		public function __construct(){
			parent::__construct('mysql:host=localhost;dbname=logbook', 'user', 'pass');
		}
	}
?>

Na sua aplicação, substitua as strings user e pass pelos valores configurados no seu servidor. Esta pequena script foi gravada no ficheiro bd.php.

Com esta extensão, para conetarmos a uma base de dados basta:

<?php
	require_once('bd.php');
	
	$bd = new myDB();
	...
?>

A linha 2 é necessária para garantirmos que a extensão da classe é carregada.

Vamos agora ver como codificaríamos os restantes serviços da nossa aplicação.


Pretende-se obter como resultado desta operação um logbook vazio. Falta definir qual a representação para um logbook vazio.

Na tutorial anterior havíamos considerado um documento XML com um elemento logs vazio como se ilustra a seguir:

<?xml version="1.0" encoding="ISO-8859-1"?>
<logs/>

Neste tutorial queremos simplesmente apagar todos os registos existentes na tabela da base de dados.

As operações na base de dados fazem-se enviando comandos SQL à mesma. O comando SQL para apagar todos os registos de uma base de dados é:

DELETE * FROM table_name 
    ou simplesmente
DELETE FROM table_name

O nosso serviço codifica-se então da seguinte forma:

<?php
	require_once('bd.php');
	
	$bd = new myDB();
	$bd->query("DELETE * FROM logs");
	
	echo "Os logs foram apagados. <br/><a href='index.html'>Voltar ao início</a>";
?>

A linha 5 ilustra o envio de um comando SQL à base de dados.


Este serviço é constituído por duas partes. Um formulário que recolhe a informação do registo e uma script que processa a informação recebida do formulário. Optou-se por usar o método get permitindo que a informação, alternativamente ao formulário, possa ser enviada diretamente pelo URL.

A seguir apresenta-se o formulário para recolha de informação.

1ª parte: o formulário

<h2>Add Log</h2>
<form action="add-log.php" method="get">
    <fieldset>
         <legend>Registo de um Log:</legend>
         Tipo: <input name="tipo" type="text" size="20"/><br/> 
         Data: <input name="data" type="text" size="10"/><br/> 
         Utilizador: <input name="utilizador" type="text" size="20"/><br/>    
         Op: <input name="operacao" type="text" size="60"/><br/>
         <input type="submit" value="Enviar"/>
    </fieldset>               
</form>

Com a utilização de umas stylesheets CSS este formulário terá o seguinte aspecto:

Se não pretendermos usar o formulário e registar diretamente uma entrada podemos tirar partido do método get que nos permite fazer a operação invocando apenas a script de atendimento com os parâmetros apropriados (este mecanismo poderá ser usado, por exemplo, para integrar este serviço numa aplicação Web).

http://servidor.uminho.pt/app/add-log.php?tipo=MMM&data=2012-11-08&utilizador=jcr&operacao=teste

Note que:

  • após o nome da script o carácter ? inicia a lista de parâmetros;
  • cada parâmetro é constituído por um par: nome e valor;
  • cada parâmetro é separado do seguinte pelo carácter &.

2ª parte: a script de atendimento

Para processar os dados do formulário é preciso desenvolver uma script que neste caso recorre à nossa classe PDO estendida para inserir o registo na base de dados através de um comando SQL.

A sintaxe do comando SQL para inserir registos numa tabela, Insert, é a seguinte:

INSERT INTO table_name
       VALUES (value1, value2, value3,....)

ou

INSERT INTO table_name (column1, column2, column3,...)
       VALUES (value1, value2, value3,....)

A script de atendimento do formulário que faz a inserção da informação recebida na base de dados apresenta-se a seguir:

<?php
	require_once('bd.php');
	
	$nvs = $_GET;
	$bd = new myDB();
		
	$qstring = "INSERT INTO logs VALUES ('".
	           $nvs['tipo']."','".$nvs['data']."','".
	           $nvs['utilizador']."','".$nvs['operacao']."')";
	
	$bd->query($qstring);	
	
	echo "Registo gravado com sucesso. <br/> <a href='index.html'>Voltar ao nicio</a>";
?>

Notas:

  • Repare na atribuição $nvs = $_GET; que permite mudar o método de envio declarado no formulário, method="get", para method="post", alterando a script de atendimento apenas nesta atribuição: $nvs = $_POST;;
  • A seguir, prepara-se uma string com a query: $qstring;
  • Que é depois enviado ao sistema de gestão da base de dados: $bd->query($qstring);.

Criação de modelos para queries

Muitos dos sistemas de gestão/processamento de bases de dados já suportam o conceito de queries preparadas ou pré-compiladas. Uma query destas pode ser considerada um modelo compilado para SQL que pode ser parametrizado com variáveis.

A principal vantagem deste tipo de queries é que a query apenas é analisada uma vez mas pode ser executada várias vezes, mesmo com parâmetros diferentes.

Veja o seguinte exemplo que realiza a mesma query mas agora criando um modelo parametrizado (neste caso como vamos fazer apenas uma query não há vantagem em fazê-lo desta forma, apenas se demonstra para fins didáticos e para poder ser usado mais à frente).

<?php
	require_once('bd.php');
	
	$nvs = $_GET;
	$bd = new myDB();
		
	$query = $bd->prepare("INSERT INTO logs VALUES (:tipo, :data, :utilizador, :operacao)");
	$query->bindValue(":tipo", $nvs['tipo'], PDO::PARAM_STR);
	$query->bindValue(":data", $nvs['data'], PDO::PARAM_STR);
	$query->bindValue(":utilizador", $nvs['utilizador'], PDO::PARAM_STR);
	$query->bindValue(":operacao", $nvs['operacao'], PDO::PARAM_STR);
	
	$query->execute();	
	
	echo "Registo gravado com sucesso. <br/> <a href='index.html'>Voltar ao nicio</a>";
?>

Notas:

  • Na linha 4, cria-se um array local para receber os dados do formulário: $nvs;
  • Na linha 5, cria-se uma instância da classe PDO estabelecendo uma ligação à base de dados;
  • Na linha 7, cria-se um modelo/template para a query que se quer realizar: método ->prepare(...);
  • Nas linhas 8 a 11, preenchem-se as variáveis do modelo com valores: método ->bindValue(...);
  • Por fim, na linha 13, manda-se executar a query preparada: método ->execute(...).

No nosso exemplo acima parametrizamos o modelo com variáveis mas podíamos tê-los omitido, parametrizando o modelo pela posição.

<?php
	require_once('bd.php');
	
	$nvs = $_GET;
	$bd = new myDB();
		
	$query = $bd->prepare("INSERT INTO logs VALUES (?, ?, ?, ?)");
	$query->bindValue( 1, $nvs['tipo'], PDO::PARAM_STR);
	$query->bindValue( 2, $nvs['data'], PDO::PARAM_STR);
	$query->bindValue( 3, $nvs['utilizador'], PDO::PARAM_STR);
	$query->bindValue( 4, $nvs['operacao'], PDO::PARAM_STR);
	
	$query->execute();	
	
	echo "Registo gravado com sucesso. <br/> <a href='index.html'>Voltar ao nicio</a>";
?>

A script é praticamente igual apenas substituímos os placeholders no modelo por ? e fizemos o bind pela posição do parâmetro: ->bindValue( posX, valor).


Este serviço irá listar numa tabela Hyper Text Markup Language a lista de logs armazenados.

O comando SQL para selecionar todos os registos duma tabela tem a seguinte forma:

SELECT * FROM table_name

Para solucionar o nosso problema, basta executar uma query com aquele comando.

O resultado duma seleção é sempre uma lista de registos. No nosso caso, quando a query é enviada com o método PDO::query o resultado é na mesma uma lista de registos que é retornado como um objecto da classe PDOStatement. Podemos iterar sobre este objeto usando a função ->fetch(). Quando os registos se esgotarem esta função retorna o valor lógico falso. Este procedimento consome recursos do lado do servidor da base de dados, provavelmente uma cache onde é mantida a lista de registos. Estes recursos devem ser libertados antes de executarmos outra query. Para isso usaremos o método PDO:closeCursor como veremos num exemplo mais à frente.

<?php
	require_once('bd.php');
	
	echo "<table border='1' style='text-align:center;'><tr><th>Tipo</th><th>Data</th><th>Utilizador</th><th>Operação</th></tr>";
	$bd =new myDB();
	$query = $bd->query("SELECT * FROM logs");
	
	while ($linha = $query->fetch()){
		echo "<tr><td>".$linha['tipo']."</td>";
		echo "<td>".$linha['data']."</td>";
		echo "<td>".$linha['utilizador']."</td>";
		echo "<td>".$linha['operacao']."</td>";
		echo "</tr>";
	}
	echo "</table><br/>  <a href='index.html'>Voltar ao ínicio</a>";
?>

Até à linha 6, cria-se uma ligação à base de dados e executa-se a query.

Uma alternativa ao método PDO::fetch seria iterarmos diretamente sobre o resultado da query, como se pode ver a seguir.

<?php
	require_once('bd.php');
	
	echo "<table border='1' style='text-align:center;'><tr><th>Tipo</th><th>Data</th><th>Utilizador</th><th>Operação</th></tr>";
	$bd =new myDB();
	$qstring = "SELECT * FROM logs";
	
	foreach ($bd->query($qstring) as $linha)
	{
		echo "<tr><td>".$linha['tipo']."</td>";
		echo "<td>".$linha['data']."</td>";
		echo "<td>".$linha['utilizador']."</td>";
		echo "<td>".$linha['operacao']."</td>";
		echo "</tr>";
	}
	echo "</table><br/>  <a href='index.html'>Voltar ao ínicio</a>";
?>

Uma das maneiras de se conseguir interoperabilidade entre aplicações é através do intercâmbio de informação em formatos bem definidos e abertos. O XML é o melhor candidato no que concerne ao formato. Assim iremos usar o formato XML desenvolvido ao longo do tutorial anterior para fazer backup da aplicação e poder recuperar o seu estado mais tarde.

Neste serviço, recuperamos um estado anterior lendo o documento XML e acrescentando a sua informação à base de dados. Para uma maior flexubilidade, o serviço foi partido em duas partes: um formulário que permite selecionar o ficheiro que se quer carregar e a script que vai processar o ficheiro.

Primeira parte: formulário para seleção do ficheiro

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Load XML</title>
  </head>
  <body>
    <form method="post" action="load_xml.php" enctype="multipart/form-data">
      <fieldset>
        <legend>Logs File:</legend> 
        <table>
          <input type='file' name='logfile'/>
        </table>
      </fieldset>
      <input type="submit" name="Enviar"/>
    </form>
  </body>
</html>

Repare na utilização do método post e no tipo associado ao formulário multipart/form-data.

Segunda parte: script de atendimento

Neste caso, a importação de registos de XML, a script fica bastante mais simples se usarmos a classe SimpleXML.

<?php

require_once('bd.php');
	
	$bd = new myDB();
	$logs = simplexml_load_file($_FILES['logfile']['tmp_name']);
	
	$query = $bd->prepare("INSERT INTO logs VALUES (:tipo, :data, :utilizador, :operacao)");
	foreach($logs->log as $l) 
	  { 
	    $query->bindValue(":tipo", (string)$l->tipo, PDO::PARAM_STR);
		$query->bindValue(":data", (string)$l->data, PDO::PARAM_STR);
		$query->bindValue(":utilizador", (string)$l->utilizador, PDO::PARAM_STR);
		$query->bindValue(":operacao", (string)$l->operacao, PDO::PARAM_STR);
		$query->execute();
		$query->closeCursor();
	}
	echo "Logs importados com sucesso. 
Voltar ao nicio"; ?>

E por fim, o último serviço da nossa aplicação.

Aqui iremos usar a classe DOM porque, como vimos no último tutorial, dá-nos mais controlo na criação do documento.

<?php
	require_once('bd.php');
	
	$bd= new myDB();

	$dom = new DOMDocument('1.0', 'ISO-8859-1');
	
	$dom->formatOutput = true;
	$dom->preserveWhiteSpace = false;
	$dom->normalizeDocument();
	
	$logs = $dom->createElement('logs');
	$dom->appendChild($logs);
			
	$query = $bd->query("SELECT * FROM logs");
	
	while ($linha = $query->fetch()){
		$element = $dom->createElement('log');
		$element->appendChild($dom->createElement('tipo', $linha['tipo']));
		$element->appendChild($dom->createElement('data', date('Y-m-d',strtotime($linha['data']))));
		$element->appendChild($dom->createElement('utilizador', $linha['utilizador']));
		$element->appendChild($dom->createElement('operacao', $linha['operacao']));
		$logs->appendChild($element);
	}

	header("Content-type: text/xml;");
	
	echo $dom->saveXML();
?>

Fica aqui terminado este tutorial onde se pretenderam demonstrar as funcionalidades básicas do processamento de bases de dados com PHP. Este conjunto de ideias e scripts pode ser facilmente adaptado para trabalhar com muitas outras aplicações.