Sistema de Tags em PHP e MySQL usando PDO, Transactions, Chaves Estrangeiras e Lambda Functions

25 dez

Neste tutorial mostrarei como executar todo o CRUD em um sistema que contenha relacionamento de conteúdo com tags. No fim do tutorial você deve adquirir conhecimento em várias técnicas muito úteis, como closures (também conhecidas como anonymous functions ou lambda functions) em PHP, transações (transaction) usando PDO e chaves estrangeiras em MySQL.

O que são tags?

Tags são uma forma de categorizar qualquer tipo de conteúdo. Esse método de ligação é usado amplamente em blogs, sites de notícias e e-commerce só pra citar alguns. A ideia é que um post/notícia/produto esteja ligado a certo assunto, e esse assunto se transforma na tag. Na imagem abaixo são exibidas algumas exemplos de sites que fazem utilização de tags.

Além de mostrar rapidamente do que se trata a notícia/post/produto, as tags em geral servem para filtrar o conteúdo e retornar somente o que interessa ao usuário.

Projeto

Neste tutorial criarei um projeto que consistirá em um pequeno sistema de blog, com posts que serão exibidos na tela. Os posts terão um título, data de criação, conteúdo e, logicamente, tags.

Antes de iniciar o projeto é fundamental ter em mente como o banco de dados será modelado. Nesse caso o relacionamento entre os posts e as tags não é tão simples. Pra entender essa última afirmação imagine as seguintes situações:

  1. Armazenar as tags dentro da tabela dos posts. Logo de cara essa aproximação não se mostra muito boa, pelos seguintes fatores:
    • É complicado saber se um post tem uma tag específica, a não ser que expressões regulares sejam usadas.
    • É difícil inserir novas tags, porque além de ter de verificar se a tag já existe (caso anterior), é necessário concatenar a nova tag com as já existentes.
  2. Armazenar as tags dentro de tabela separada. Essa situação também não é das melhores.
    • Se um post tiver mais de uma tag, cairemos no mesmo problema já citado, onde as tags teriam de ser concatenadas na tabela dos posts, mas dessa vez usando ‘id’ ao invés de seu valor original.
  3. Armazenas as tags e os posts em tabelas independentes, e criar uma terceira tabela que servirá como ponte entra as duas anteriores. Essa é a situação ideal em geral, pois facilita bastante no momento de manipular os dados como será visto mais adiante. Esse tipo de modelagem se chama muitos-para-muitos (many-to-many).

A modelagem ficará da seguinte maneira

Os campos post_id e tag_id serão chaves estrangeiras nesta modelagem. Criar o banco de dados não é nada complexo. As consultas a serem executadas são as seguintes:

CREATE DATABASE `tc_blog`;

CREATE TABLE posts (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    titulo VARCHAR(200) NOT NULL UNIQUE,
    data_criacao TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    conteudo TEXT NOT NULL
) ENGINE = INNODB;

CREATE TABLE tags (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(50) NOT NULL UNIQUE,
    INDEX(nome)
) ENGINE = INNODB;

CREATE TABLE posts_tags (
    post_id INT NOT NULL,
    tag_id INT NOT NULL ,
    PRIMARY KEY(post_id, tag_id),
    FOREIGN KEY(post_id) REFERENCES posts(id) ON DELETE CASCADE ON UPDATE CASCADE,
    FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = INNODB;

Decidi que o título do post e o nome das tags não podem se repetir, por isso a cláusula UNIQUE em ambos. Como a tabela posts_tags tem duas chaves primárias, a combinação das duas deve ser sempre única, ou seja, um post não pode aparecer relacionado à mesma tag mais de uma vez. Agora basta inserir algum conteúdo para que o projeto possa prosseguir.

INSERT INTO posts(titulo, data_criacao, conteudo) VALUES 
('primeiro post', '2010-10-11 13:00:01', 'conteúdo do primeiro post'),
('segundo post', '2010-10-12 13:10:00', 'conteúdo do segundo post'),
('terceiro post', '2010-10-13 13:20:00', 'conteúdo do terceiro post'),
('quarto post', '2010-10-14 13:30:00', 'conteúdo do quarto post');

INSERT INTO tags(nome) VALUES 
('php'), ('mysql'), ('javascript'), ('html'), ('css');

Só resta associar os posts às tags. A distribuição ficará da seguinte maneira:

Primeiro post
tags: php, mysql
Segundo post
tags: php
Terceiro post
tags: (nenhuma tag)
Quarto post
tags: javascript, html, php, css

A consulta se inserção fica da seguinte maneira:

INSERT INTO posts_tags(post_id, tag_id) VALUES
(1,1), (1,2),
(2,1), 
(4,1), (4,3), (4,4), (4,5) 

Estrutura do blog

A estrutura para este tutorial é bem simples e não se preocupa com nenhum quesito de segurança, apesar desse assunto ser fundamental em um aplicativo real, a ideia é tornar o tutorial mais simples.

Os nomes dos arquivos já falam bastante sobre o que se esperar de cada um deles, então não acredito que seja necessário uma explicação detalhada de cada um.

Conexão com o banco de dados

A conexão com o banco de dados será realizada no arquivo conexao.php, na pasta inc, como visto a seguir:

$host = 'localhost';
$user = 'root';
$pass = '';
$database   = 'tc_blog';

$options = array(
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);

$db = new PDO('mysql:host='.$host.';dbname='.$database, $user, $pass, $options);
unset($host,$user,$pass,$database,$options);

Esta conexão será compartilhada por todas as páginas, por isso minha decisão por criar um arquivo que contivesse toda a lógica. Na última linha deletei as variáveis de conexão pra ter certeza que elas não vão ter influência posteriormente no script.

Selecionando os posts

As coisas começam a complicar um pouquinho aqui, já que tenho que interagir com três tabelas pra poder exibir todas as informações do post. Antes de mostrar todo o conteúdo da página vou escrever a SQL e explicar como ela funciona:

SELECT posts.id, posts.titulo, posts.data_criacao, posts.conteudo, group_concat( DISTINCT tags.nome SEPARATOR ‘, ’) AS tags
FROM posts
LEFT JOIN posts_tags ON posts.id = posts_tags.post_id
LEFT JOIN tags ON tags.id = posts_tags.tag_id
GROUP BY posts.titulo
ORDER BY data_criacao DESC

O objetivo desta query é selecionar tudo de uma só vez, ou seja, evitar múltiplas consultas ao banco de dados. Vou explicar o código linha a linha, por não se tratar de algo trivial.

Linha 1

seleciono os campos id, titulo, data_criacao e conteudo da tabela posts, e por último concateno as tags usando uma vírgula como separador. Pra fazer isso uso a função GROUP_CONCAT.

A função GROUP_CONCAT aceita três cláusulas:

DISTINCT
Usado para selecionar somente valores únicos, ou seja, se a consulta encontrar duas vezes a mesma tag, o resultado final exibirá cada uma somente uma vez, exatamente o que queremos. Se esta cláusula for omitida, retornos duplicados são permitidos.
ORDER BY
serve para ordenar o resultado de forma ascendente (padrão) ou descendente. Se esta cláusula for omitida, o padrão ASC (ascendente) será considerado.
SEPARATOR
Define o separador usado para concatenar o resultado. Se esta cláusula for omitida, o separador padrão (uma vírgula) será usado.

No caso da consulta anterior, não usei a cláusula ORDER BY, então os resultados virão na ordem que aparecem no banco de dados.

Linhas 2, 3 e 4

Digo que estou selecionando os dados das tabelas posts, posts_tags e tags, mas somente onde o campo id da tabela posts é igual ao campo post_id da tabela posts_tags, e também onde o campo id da tabela tags é igual ao campo tag_id da tabela posts_tags.

Repare que uso a cláusula LEFT JOIN. Faço isso porque um post pode não ter tags associadas. Se eu usasse INNER JOIN, os posts que não tem tags não apareceriam como resultado desta consulta.

Linha 5

Aqui digo que quero agrupar os dados pelo titulo do post. A escolha do campo não é importante, desde que esse campo seja único, ou seja, estou supondo que dois posts nunca terão o mesmo título (como definido na criação da tabela).

Linha 6

Enfim uma parte simples! Aqui só ordeno os resultados pela data de criação de forma descendente, ou seja, os mais recentes aparecem primeiro.

Com o pleno entendimento da SQL já é possível partir para a exibição dos resultados. Caso você não tenha entendido perfeitamente o que foi feito até agora, volte e releia os passos, porque todo o sistema de exibição depende do que foi feito até agora.

Página principal

Esta será a página principal deste blog, ou seja, o local onde serão exibidos todos os posts. Com a consulta criada no item anterior, já é possível exibir todos os posts com suas respectivas tags.

<?php
    // Não se esqueça de incluir a conexão!! 
    require_once 'inc/conexao.php';
    include_once 'inc/functions.php';
        
    $sql = <<<SQL
    SELECT p.id, p.titulo, p.data_criacao, p.conteudo, group_concat( DISTINCT t.nome SEPARATOR ', ') AS tags
	FROM posts p
	LEFT JOIN posts_tags pt ON p.id = pt.post_id
	LEFT JOIN tags t ON t.id = pt.tag_id
	GROUP BY p.id
	ORDER BY p.data_criacao DESC
SQL;

    $stmt = $db->prepare($sql);
    $stmt->execute();
    $stmt->setFetchMode(PDO::FETCH_OBJ);
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1">
	<title>TC Blog - Página Principal</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" />
</head>
<body>
	<?php while ($result = $stmt->fetch()): ?>
	<div class="post">
		<h1>
			<a href="post.php?id=<?php echo $result->id ?>"><?php echo $result->titulo ?></a>
		</h1>
		<p><em><?php echo $result->data_criacao; ?></em></p>
		<p><?php echo $result->conteudo; ?></p>
		<?php if($tags = $result->tags ): ?>
			<p>Tags: <em><?php echo $tags ?></em></p>
		<?php endif; ?>
		<p><a href="atualizar-post.php?id=<?php echo $result->id ?>">editar</a></p>
	</div>
	<?php endwhile; ?>
</body>
</html>

Vou explicar o funcionamento do script com detalhes.

Linhas 1 até 18

Não existe nada de muito especial aqui, só incluo a conexão, crio a SQL e executo. Nesse caso usei HEREDOCS pra construção da minha variável sql, mas isso não é obrigatório, tendo em vista que é o mesmo que usar aspas duplas.

Nas linhas 15 e 16 eu preparo e execute minha consulta. Como não tirei proveito de prepared statements, a preparação da consulta é opcional. Na linha 17 explicito a minha intenção do resultado retornar como objeto. Novamente minha decisão é opcional, você pode usar outro modo se achar mais conveniente.

Linhas 28 até 40

Faço o loop com os resultados retornados do banco de dados. Não há grandes novidades nesse trecho. A declaração if é usada porque um post pode não ter tags associadas. Se nenhuma tag for encontrada eu não quero que os elementos, que envolvem as tags, apareçam. A variável tags terá valor null se nenhum resultado retornar do banco dados.

Página Post

Aqui será exibido um post, que vai ser selecionado através de uma query string. A lógica é bastante simples, e bem próxima da lógica presente no arquivo principal.php. Com um pouco mais de trabalho é possível unir esses dois arquivos em um, mas pra tornar o tutorial mais simples decidi manter essa divisão.

<?php
    // Não se esqueça de incluir a conexão!! 
    require_once 'inc/conexao.php';
    
    $input_options = array(
    	'min_range'=>1
    );
    if(!$post_id = filter_input(INPUT_GET, 'id',FILTER_VALIDATE_INT,$input_options))
    {
    	die('Post não encontrado!');
    }
    
    $sql = <<<SQL
    SELECT p.id, p.titulo, p.data_criacao, p.conteudo, group_concat( DISTINCT t.nome SEPARATOR ', ') AS tags
	FROM posts p
	LEFT JOIN posts_tags pt ON p.id = pt.post_id
	LEFT JOIN tags t ON t.id = pt.tag_id
	WHERE p.id = :post_id
SQL;

    $stmt = $db->prepare($sql);
    $stmt->bindValue(':post_id', $post_id, PDO::PARAM_INT);	
    $stmt->execute();
    $stmt->setFetchMode(PDO::FETCH_OBJ);
    $result = $stmt->fetch();
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
	<div class="post">
		<h1><?php echo $result->titulo; ?></h1>
		<p><em><?php echo $result->data_criacao; ?></em></p>
		<p><?php echo $result->conteudo; ?></p>
		<?php if($tags = $result->tags ): ?>
			<p>Tags: <em><?php echo $tags ?></em></p>
		<?php endif; ?>
	</div>
</body>
</html>

A diferença dessa página para a página principal são mínimas. Na linha 8 verifico a existência da variável super global $_GET[‘id’] (usando a função filter_input), se existir será assinalada à variável $post_id, senão a execução da página será paralisada e uma mensagem amigável será exibida. Repare que aproveito para fazer uma rápida validação da variável passada pela URL, definindo que deve ser um valor inteiro e maior que zero. Na SQL adicionei a cláusula WHERE, para filtrar pela id do post que quero selecionar.

Criar Tag

Esta página é bem simples, e servirá somente para que possamos inserir tags no nosso banco de dados.

<?php
if($_SERVER['REQUEST_METHOD'] === 'POST' AND $tag = filter_input(INPUT_POST,'tag'))
{
    // Não se esqueça de incluir a conexão!! 
    require_once 'inc/conexao.php';
    
    try
    {
        $sql = 'INSERT INTO tags(nome) VALUES (:tag)';
        $stmt = $db->prepare($sql);
        $stmt->bindValue(':tag', $tag, PDO::PARAM_STR);
        $result = $stmt->execute();
        $message = 'Tag inserida com sucesso!';
    }
    catch (PDOException $e)
    {
        if($e->getCode() == 23000)
        {
            $message = 'Esta tag já existe!';
        }
    }
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog - Criar Tag</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <?php if(isset($message)):?>
		<p><strong><?php echo $message ?></strong></p>
	<?php endif;?>
	<h1>Criar Tag</h1>
	<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
		<div>
			<label for="tag">Tag</label><input type="text" name="tag" id="tag" />
		</div>
		<div><input type="submit" name="submit" value="Criar Tag" /></div>
	</form>
</body>
</html>

Se o correr um erro na inserção da tag é bastante provável que esse erro seja de tag duplicada, já que na criação da tabela tags eu defini que elas teriam de ser únicas. Eu poderia usar a cláusula IGNORE na inserção da tag, ela serve para ignorar erro de duplicação em colunas que devem ter valores únicos. Como quero retornar uma mensagem pro usuário caso a tag já exista, envolvi a consulta em um bloco try/catch, para que a mensagem de erro possa ser exibida.

Deletar Tag

<?php
// Não se esqueça de incluir a conexão!! 
require_once 'inc/conexao.php';

$input_options = array(
	'min_range'=>1
);
if( $_SERVER['REQUEST_METHOD'] === 'POST' 
	AND 
	$tag_id = filter_input(INPUT_POST,'tag',FILTER_VALIDATE_INT,$input_options))
{
    // Deletar a tag e todas as referências à ela na tabela posts_tags
    $sql = 'DELETE FROM tags WHERE tags.id = :tag_id';
    $stmt = $db->prepare($sql);
    $stmt->bindValue(':tag_id', $tag_id, PDO::PARAM_INT);
    $stmt->execute();
    unset($stmt);
}

$sql = 'SELECT id, nome FROM tags';
$stmt = $db->prepare($sql);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_OBJ);
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog - Deletar Tag</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
	<h1>Deletar Tag</h1>
	<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
		<div>
			<label for="tag">Tag</label>
			<select name="tag" id="tag">
				<?php while ($result = $stmt->fetch()):?>
					<option value="<?php echo $result->id ?>"><?php echo $result->nome ?></option>
				<?php endwhile;?>
			</select>
		</div>
		<div><input type="submit" name="submit" value="Deletar Tag" /></div>
	</form>
</body>
</html>

Esta página é ainda mais simples que a anterior. No corpo da página coloco todas as tags em um campo select, para que possa escolher qual tag deletar.

Se a requisição for do tipo POST, então pulo pra parte onde a tag é deletada. A SQL a ser executada é trivial, e se aproveita do fato de eu ter criado referências entre as tabelas.

Quando usei a cláusula ON DELETE CASCADE na criação da tabela posts_tags a minha intenção era que eu não precisasse usar uma lógica no PHP pra deletar as referências nesta tabela, ou seja, deletando uma tag na tabela tags automaticamente as tags que se ligam a ela na tabela posts_tags serão deletadas. Se eu não tivesse feito isso eu teria que executar mais de uma consulta, ou, alternativamente, usar multi delete e fazer tudo de uma vez.

Atualizar Tag

<?php
// Não se esqueça de incluir a conexão!! 
require_once 'inc/conexao.php';

if($_SERVER['REQUEST_METHOD'] === 'POST')
{
	$input_options = array(
		'min_range'=>1
	);
    if($id = filter_input(INPUT_POST,'tag',FILTER_VALIDATE_INT,$input_options) 
    	AND 
       $nome = filter_input(INPUT_POST, 'nome'))
    {
        $sql = 'UPDATE tags SET nome = :nome WHERE id = :id';
        $stmt = $db->prepare($sql);
        $stmt->bindValue(':nome', $nome, PDO::PARAM_STR);
        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        unset($stmt);
    }
}

$sql = 'SELECT id, nome FROM tags';
$stmt = $db->prepare($sql);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_OBJ);
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog - Atualizar Tag</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
	<h1>Atualizar Tag</h1>
	<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
		<div>
			<label for="tag">Tag</label>
			<select name="tag" id="tag">
				<?php while ($result = $stmt->fetch()):?>
					<option value="<?php echo $result->id ?>"><?php echo $result->nome ?></option>
				<?php endwhile;?>
			</select>
		</div>
		<div>
			<label>Novo Nome</label>
			<input type="text" name="nome" id="nome" />
		</div>
		<div><input type="submit" name="submit" value="Atualizar Tag" /></div>
	</form>
</body>
</html>

Até aqui nada foi tão simples e direto quanto essa página. A lógica é pegar a tag selecionada no campo select do HTML e atualizar pro nome que o usuário inserir no campo tipo texto.

Criar Post

Chegou a hora de arregaçar as mangas e fazer algo um pouco mais complexo!

<?php
if($_SERVER['REQUEST_METHOD'] === 'POST')
{
    // Não se esqueça de incluir a conexão!! 
    require_once 'inc/conexao.php';
    
    // Pegando conteúdo enviado pelo usuário
    $titulo   = filter_input(INPUT_POST, 'titulo');
    $conteudo = filter_input(INPUT_POST, 'conteudo');
    
    // Preparando as tags. Cada uma será um elemento de um array
    $tags = array_map(function($value){
        return trim($value);
    }, explode(',', filter_input(INPUT_POST, 'tags')));
    
    try
    {
        $db->beginTransaction();
        
        $sql_insert_post = 'INSERT INTO posts(titulo,conteudo) VALUES (:titulo,:conteudo)';
        $stmt = $db->prepare($sql_insert_post);
        $stmt->bindValue(':titulo'  , $titulo  , PDO::PARAM_STR);
        $stmt->bindValue(':conteudo', $conteudo, PDO::PARAM_STR);
        $stmt->execute();
        $post_id = $db->lastInsertId();
        unset($stmt);
        
        // Se tiver alguma tag...
        if($tagCount = count($tags))
        {
            // 1º Passo: inserir as tags em sua tabela
            foreach ($tags as $tag)
            {
            	$sql_insert_tags = 'INSERT IGNORE INTO tags(nome) VALUES (?) ';
            	$stmt = $db->prepare($sql_insert_tags);
            	$stmt->execute(array($tag));
            	unset($stmt,$sql_insert_tags);
            }

            // 2º passo: selecionar as IDs das tags escolhidas pelo usuário
            $sql_select_ids = 'SELECT id FROM tags WHERE nome IN(';
            reset($tags);
            foreach ($tags as $tag)
            {
            	$sql_select_ids .= $db->quote($tag,PDO::PARAM_STR) . ',';
            }
            //Remover última vírgula e adicionar último parêntese
            $sql_select_ids = substr($sql_select_ids, 0, strlen($sql_select_ids) - 1) . ')';
            $stmt = $db->query($sql_select_ids);
            $tags_ids = $stmt->fetchAll(PDO::FETCH_OBJ);
            unset($stmt);
            
             // 3º Passo: Inserir as referências na tabelas posts_tags
            $sql_insert_posts_tags = 'INSERT INTO posts_tags(post_id,tag_id) VALUES ';
            foreach ($tags_ids as $tag_id)
            {
                $sql_insert_posts_tags .= '(';
                $sql_insert_posts_tags .= $db->quote($post_id,PDO::PARAM_INT);
                $sql_insert_posts_tags .= ',';
                $sql_insert_posts_tags .= $db->quote($tag_id->id,PDO::PARAM_INT);
                $sql_insert_posts_tags .= '),';
            }
            // Remover última vírgula
            $sql_insert_posts_tags = substr($sql_insert_posts_tags, 0, strlen($sql_insert_posts_tags) - 1);
            $db->query($sql_insert_posts_tags);
        }
        
        $db->commit();
    }
    catch (PDOException $e)
    {
        $db->rollBack();
        die($e->getMessage());
    }
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog - Criar Post</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
	<h1>Criar Post</h1>
	<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
		<div>
			<label for="titulo">Título</label><input type="text" name="titulo" id="titulo" />
		</div>
		<div>
			<label for="conteudo">Conteúdo</label><textarea rows="10" cols="25" name="conteudo" id="conteudo"></textarea>
		</div>
		<div>
			<label for="tags">Tags <small>(separe com vírgulas)</small></label><input name="tags" id="tags" />
		</div>
		<div><input type="submit" name="submit" value="Criar Post" /></div>
	</form>
</body>
</html>

Linhas 87 até 98

Aqui criei campos para a inserção dos dados do post, que são título, conteúdo e tags. Na criação do post é possível inserir várias tags separando-as com vírgulas.

Linha 2

Voltando ao início do script, onde rola toda a lógica, a minha primeira ação é verificar se a requisição é do tipo POST. Se for, é porque um post está sendo criado.

Linha 8 e 9

Ao invés de usar diretamente a variável superglobal $_POST, uso a função filter_input, que no fim das contas dá no mesmo, então cabe a você escolher qual vai usar.

Linhas 12 até 14

Nesse ponto rola uma pequena mágica. Na versão 5.3 do PHP é possível usar funções anônimas como argumento de outras funções, e tiro proveito disso nessa parte do código. A função array_map serve para executar certa ação a cada elemento da array, e o que retornar, da função que foi passada como parâmetro, será assinalado ao valor do elemento atual da array. O segundo parâmetro desta função é a array que desejamos trabalhar. Pra manter o código mais compacto eu reuni tudo em um só lugar.

As tags vêm como uma string no nosso POST, então eu explodo essa string na posição das vírgulas, e em seguida removo os espaços em branco usando a função trim (que está na função anônima). O que retorna desse trecho é uma array com as tags que eu quero inserir no banco de dados, e, em seguida, associar ao post.

Esse é um momento delicado no nosso script, porque vou executar várias consultas, e elas estão associadas entre si. O que acontece se eu inserir o post, e em seguida não conseguir associar as tags ao post? Esse é um problema sério, mas a solução é bem simples, basta usar transactions.

Na maior parte dos bancos de dados relacionais temos a possibilidade de usar transactions, e elas são úteis em dois aspectos, primeiro porque evitam erros como o que citei anteriormente, segundo porque em algumas em ocasiões elas melhoram o desempenho na execução da SQL, isso acontece pelo fato dela executar tudo de uma só vez, ao invés fazer várias execuções.

Em MySQL a engine MyISAM não suporta transactions! Repare que todas as tabelas desse tutorial foram criadas usando a engine InnoDB, pelo fato de não terem o problema citado anteriormente, além de também suportarem chaves estrangeiras. Se você criar as tabelas usando a engine MyISAM, os métodos PDO::beginTransaction, PDO::commit e PDO::rollBack não surtirão qualquer efeito (nem mensagens de erro aparecerão).

Usando transactions nós podemos verificar se existe algum erro no caminho, e caso isso aconteça podemos desfazer o que já foi feito. Lançar mão de transactions usando PDO é uma simples questão de usar o método PDO::beginTransaction. Para confirmar que tudo ocorreu da forma prevista devemos usar o método PDO::commit, e para desfazer o que já foi feito (caso ocorra algum erro), usamos o método PDO::rollBack.

Uso o bloco try/catch para verificar se existem erros no momento da execução. Se nada de mal ocorrer, chegamos ao fim do bloco try, onde confirmo que tudo deu certo, usando PDO::commit. Se algum erro ocorrer, imediatamente o código pula para o catch, onde executo o método PDO::rollback, para desfazer tudo que foi feito.

Linhas 20 até 25

A primeira SQL a ser executada é da inserção do post, que sempre vai acontecer. Eu posso não passar tags, mas o post sempre terá que ser criado. Nesta parte só gostaria de chamar a atenção para a linha 25, onde resgato o valor da id do post que acabou de ser inserido. Essa id será usada mais pra frente.

O script da linha 30 até a linha 66 só será executado se o usuário enviar alguma tag no formulário.

Linhas 33 até 38

insiro todas as tags no banco de dados. Se o número de tags for grande é possível que o tempo de execução aumente, mas como estou usando transaction esse efeito é bastante atenuado.

Linhas 41 até 50

Nesse trecho volto ao banco de dados pra resgatar as ids das tags passadas pelo usuário. Como a única informação que tenho das tags são seus nomes, a ideia é filtrar os resultados usando a cláusula IN na minha SQL. Dentro do loop foreach concateno cada uma das tags com a SQL que comecei a criar na linha 41. Repare que usei o método PDO::quote. Esse método coloca aspas em torno da string.

Na linha 48 removo o último caractere, que é uma vírgula inserida no ultimo loop foreach, em seguida adiciono um parêntese para finalizar a SQL.

Para pegar todas as tags de uma só vez uso o método PDOStmt::fetchAll, caso contrário teria de fazer um loop para obter o valor de cada tag.

Linhas 54 até 65

Começo criando a SQL que será responsável por inserir as referências na tabela posts_tags. Nesse momento só preciso da id do post mais recente e das ids das tags inseridas. A id do post foi capturada na linha 25, e as ids das tags foram capturadas no trecho imediatamente anterior do código.

Na linha 54 inicio a SQL responsável por inserir as referências na tabela posts_tags. No loop foreach crio cada par de associações post/tag. Na linha 64 retiro o último caractere (uma vírgula) e em seguida executo a consulta.

Se até aqui nada ocorreu de errado, tudo que foi feito anteriormente será executado na linha 68. Caso contrário tudo será desfeito na linha 72.

Deletar Post

<?php
// Não se esqueça de incluir a conexão!! 
require_once 'inc/conexao.php';

$input_options = array(
	'min_range'=>1
);
if($_SERVER['REQUEST_METHOD'] === 'POST' 
	AND 
   $post_id = filter_input(INPUT_POST,'post',FILTER_VALIDATE_INT,$input_options))
{
    // Deletar o post e todas as referências à ele na tabela posts_tags
    $sql = 'DELETE FROM posts p WHERE p.id = :post_id';
    $stmt = $db->prepare($sql);
    $stmt->bindValue(':post_id', $post_id, PDO::PARAM_INT);
    $stmt->execute();
}

$sql = 'SELECT id, titulo FROM posts';
$stmt = $db->prepare($sql);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_OBJ);
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog - Deletar Post</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
	<h1>Deletar Post</h1>
	<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
		<div>
			<label for="post">Título</label>
			<select name="post" id="post">
				<?php while ($result = $stmt->fetch()):?>
					<option value="<?php echo $result->id ?>"><?php echo $result->titulo ?></option>
				<?php endwhile;?>
			</select>
		</div>
		<div><input type="submit" name="submit" value="Deletar Post" /></div>
	</form>
</body>
</html>

Esta página é bastante simples, e se aproveita dos relacionamentos criados entre as tabelas no início do tutorial. Se a requisição for do tipo GET, o trecho das linhas 11 até 17 não será executado. Da linha 19 até a 22 faço uma seleção da id e do título dos posts, para que possa preencher um menu select (linha 37 até 41). Se a requisição for do tipo POST e existir a variável superglobal $_POST[‘post’], o trecho da linha 11 até 17 será executado. Em seguida faço uma consulta simples para deletar o post.

Nesse momento uma mágica acontece no banco de dados. Se o post deletado tiver alguma tag associada, todas as referências na tabela posts_tags serão deletadas! Isso acontece graças a cláusula ON DELETE CASCADE, usada na criação da tabela posts_tags.

Atualizar Post

<?php

// Não se esqueça de incluir a conexão!! 
require_once 'inc/conexao.php';

if($_SERVER['REQUEST_METHOD']=='POST')
{
    try {
        $db->beginTransaction();
        
        $input_options = array(
        	'min_range'=>1
        );
        $post_id  = filter_input(INPUT_POST, 'id',FILTER_VALIDATE_INT,$input_options);
        $titulo   = filter_input(INPUT_POST, 'titulo');
        $conteudo = filter_input(INPUT_POST, 'conteudo');
        
        // 1º passo: atualizar a tabela do post
        $sql = 'UPDATE posts SET titulo = :titulo, conteudo = :conteudo WHERE id = :id';
        $stmt = $db->prepare($sql);
        $stmt->bindValue(':titulo',$titulo,PDO::PARAM_STR);
        $stmt->bindValue(':conteudo',$conteudo,PDO::PARAM_STR);
        $stmt->bindValue(':id',$post_id,PDO::PARAM_INT);
        $stmt->execute();
        unset($stmt,$sql);
        
        // 2º passo: deletar todas as referências das tags ao post a ser atualizado
        $sql = 'DELETE FROM posts_tags WHERE post_id = ?';
        $stmt = $db->prepare($sql);
        $stmt->bindValue(1,$post_id,PDO::PARAM_INT);
        $stmt->execute();
        unset($stmt,$sql);
        
        $tags = explode(',',filter_input(INPUT_POST,'tags'));
        $tags = array_map(function($val){
            return trim($val);
        } , $tags);
        $tags = array_filter(array_unique($tags));
         
        $tagCount = count($tags);
        if($tagCount)
        {
			// 3º passo: inserir as as tags no banco de dados (caso não existam)
	        $placeholders = array_map(function($val){
	            global $db;
	            return '('.$db->quote($val).')';
	        }, $tags);
	        $sql  = 'INSERT IGNORE INTO tags(nome) VALUES ';
	        $sql .= implode(',', $placeholders);
	        $db->query($sql);
	        unset($sql);
	        
	        $db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, TRUE);
            
	        // 4º passo: Inserir as referências na tabela posts_tags
            // Pegar todas as tags escolhidas pelo usuário
            $placeholders = array_fill(0,$tagCount,'?');
            $sql = 'SELECT id FROM tags WHERE nome IN('.implode(',', $placeholders).')';
            $stmt = $db->prepare($sql);
            unset($sql);
            for($i = 1; $i <= $tagCount; $i++)
            {
                $stmt->bindValue($i, $tags[$i-1]);
            }
            $stmt->execute();
            
            if($stmt->rowCount())
            {
                // Inserir as referências em posts_tags
                $sql = 'INSERT INTO posts_tags(post_id,tag_id) VALUES ';
                $stmt->setFetchMode(PDO::FETCH_OBJ);
                while ($result = $stmt->fetch())
                {
                    $sql .= '(';
                    $sql .= $db->quote($post_id,PDO::PARAM_INT);
                    $sql .= ',';
                    $sql .= $db->quote($result->id,PDO::PARAM_INT);
                    $sql .= '),';
					// Ex.: ('1','2'),
                }
                
                // Retirar último cacactere (vírgula)
                $sql = substr($sql, 0, strlen($sql) - 1);
            }
            unset($stmt);
            
            $db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,FALSE);
            $stmt = $db->prepare($sql);
            $stmt->execute();
            unset($sql);
        }
        
        $db->commit();
    }
    catch (PDOException $e) 
    {
        $db->rollBack();
        die($e->getMessage());
    }
    header('Location: http://localhost/tc-blog/atualizar-post.php?id='.$post_id);
}

$sql = <<<SQL
    SELECT p.id, p.titulo, p.conteudo, group_concat( DISTINCT t.nome SEPARATOR ', ') AS tags
	FROM posts p
	LEFT JOIN posts_tags pt ON p.id = pt.post_id
	LEFT JOIN tags t ON t.id = pt.tag_id
	WHERE p.id = ?
SQL;

$input_options = array(
	'min_range'=>1
);
$id = filter_input(INPUT_GET,'id',FILTER_VALIDATE_INT,$input_options); 
$stmt = $db->prepare($sql);
$stmt->bindValue(1, $id,PDO::PARAM_INT);
$stmt->execute();
$stmt->setFetchMode(PDO::FETCH_OBJ);
$result = $stmt->fetch();

?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
	<meta charset="ISO-8859-1" />
	<title>TC Blog - Atualizar Post</title>
	<base href="http://localhost/tc-blog/" />
	<link href="css/style.css" rel="stylesheet" type="text/css" />
</head>
<body>
	<h1>Atualizar Post</h1>
	<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="post">
		<div>
			<label for="titulo">Título</label>
			<input type="text" name="titulo" id="titulo" value="<?php echo $result->titulo ?>" />
		</div>
		<div>
			<label for="conteudo">Conteúdo</label>
			<textarea name="conteudo" id="conteudo"><?php echo $result->conteudo ?></textarea>
		</div>
		<div>
			<label for="tags">Tags</label>
			<input type="text" name="tags" id="tags" value="<?php echo $result->tags ?>" />
		</div>
		<input type="hidden" name="id" value="<?php echo $result->id ?>" />
		<div><input type="submit" name="submit" value="Atualizar Post" /></div>
	</form>
</body>
</html>

A lógica contida nessa página é, de longe, a mais complexa, apesar de não ser nada de outro mundo.

Linhas 103 a 149

Vou começar pelo fim, que é a parte mais simples. Se a requisição for do tipo GET, o início do código não vai ser executado (linhas 7 até 101), então começamos na linha 103. O primeiro passo é selecionar o post que vai ser atualizado, pra que ele possa ser exibido na tela e as mudanças possam ser feitas.

A SQL é bem parecida com a usada na página principal, se diferindo somente na cláusula WHERE, que recebe a id do post a ser atualizado, e da cláusula ORDER BY, que não aparece aqui, além da cláusula GROUP BY, que aqui é desnecessária. Nas linhas 115 até 119 faço uma seleção simples do post e suas tags, para que possam ser exibidas no formulário de atualização.

Linhas 132 a 147

O formulário é bastante simples, e contém somente campos para o título, conteúdo e tags, além do campo hidden, com a id do post, e botão submit. As tags são separadas por vírgula (uma escolha minha).

Linhas 6 a 16

Agora vamos á lógica de atualização. Se a requisição for do tipo POST, quer dizer que o sistema está tentando fazer uma atualização, e as linhas 7 até 101 serão executadas. A primeira ação que tomo é iniciar uma transação (transaction), a fim de evitar que dados corrompidos sejam inseridos no banco de dados, como por exemplo tags associadas a posts que não estão presentes na tabela posts_tags. Repare que resgato as variáveis passadas pelo POST usando a função filter_input

Linhas 19 a 24

A primeira atualização que faço é na tabela de posts, já que é a mais simples, dependendo somente do título e do conteúdo passados nos campos do formulário. A id do post vem do campo hidden.

Linhas 28 a 31

A segunda atualização é feita em dois passos. O primeiro consiste em deletar todas as referências do post às suas tags antigas, para que em seguida essas referências possam ser refeitas. É importante que todas as referências antigas sejam apagadas, caso isso não seja feito o post poderá apresentar tags que não deveriam existir, simplesmente pelo fato das referências não terem sido excluídas. Essas ações são tomadas nas linhas 22 até 41.

Linhas 34 a 41

Preparo as tags vindas do formulário, para inserção no banco de dados. Explodo a string na vírgula (caractere de separação das tags), e em seguida excluo os espaços em branco usando a função trim. Novamente faço o uso de funções anônimas, já mencionadas anteriormente nesse tutorial. A função array_filter é usada pra remover itens que são avaliados como falso, nesse caso específico são strings vazias. Na linha 41 verifico se existe alguma tag. Isso é necessário para que eu não tente inserir algo que nem foi passado pelo usuário.

Linhas 44 a 50

Coloco todas as tags entre aspas, para que possam ser inserids no banco de dados, além coloca-las entre parênteses, para facilitar no momento de concatenar com a SQL de inserção (linhas 48 a 50). Repare que a SQL contém a cláusula IGNORE, que evita a exibição de um erro caso uma tag já exista no banco de dados.

Linhas 57 a 65

Como último passo devemos inserir as ligações dos posts com as tags na tabela posts_tags. O primeiro passo é saber quais são as ids das tags enviadas pelo usuário. A solução é fazer uma busca pela id na tabela tags, onde o nome da tag foi passada pelo usuário no formulário. Como quero fazer o uso de prepared statements, o primeiro passo é inserir as interrogações na SQL e prepara-la. Pra isso eu crio uma uma variável chamada $placeholders, que vai ser um array com o mesmo número de elementos da variável $tags. Esse processo é bem simples, e só depende da execução da função array_fill.

Em seguida fica a lógica que vai jogar o nome das tags para o SQL, usando o método PDOStmt::bindValue, e por fim executar a consulta. O resultado esperado é uma lista de ids das tags escolhidas pelo usuário.

Linhas 67 a 84

Na linha 67 verifico se algum resultado foi retornado, usando o método PDO::rowCount, por isso habilitei o armazenamento dos resultados em buffer na linha 53.

Se o armazenamento de resultados em buffer não estiver habilitado, o método PDO::rowCount sempre retornará zero como número de resultados, valor geralmente incorreto!

Das linhas 70 até 89 eu crio e executo a SQL responsável por inserir as referências na tabela posts_tags. Pra cada resultado retornado, da consulta anterior, eu concateno o id do post (passado pelo campo hidden), com a id de cada tag (que acabei de pegar), usando o método PDO::quote e envolvendo-as entre parênteses.

Como no fim de cada resultado eu insiro uma vírgula, é importante retira-la no fim de todas as consultas, e isso é tão simples quanto executar a função substr. É importante ressaltar que o último parâmetro dessa função é o tamanho da string decrementada de uma unidade, exatamente pra retirar o último caractere.

Dando tudo certo, a consulta é praparada e executada, e por fim o método PDO::commit é executado, finalizando a atualização. Se alguma coisa desse errado no meio desse script nada seria inserido no banco de dados.

Na linha 100 redireciono o usuário pra própria página, somente pra transformar a requisição em GET ao invés de POST, mas isso é opcional.

CSS

A folha de estilo não é fundamental pra esse tutorial, mas vou deixar aqui caso você seja curioso. Para mais detalhes sobre a propriedade box-sizing peço que assista ao tutorial em vídeo do Jeffrey Way no Net.Tutsplus.

*{
	-moz-box-sizing:border-box;
	-webkit-box-sizing:border-box;
	box-sizing:border-box;	
}

body{
	font-family: verdana, arial, sans-serif;
	font-size:14px;
}

form div{
	margin:10px 0;
}

label{
	display: block;
	width: 100px;
	float: left;
	text-align:right;
	padding-right:10px;
}

textarea, input{
	display:block;
	width:350px;
	height:30px;
	border:2px solid #333;
	border-radius:5px;
}

textarea:hover, textarea:focus, input:hover, input:focus, input:active{
	border-color:#D1CA04;
	outline:none;
}

textarea{
	height:140px;
}

input[type=submit]{
	width:auto;
	height:auto;
	padding:10px;
	margin-left:100px;
	background: #e6e6e6;
}

a{color:#E8B351;text-decoration:none}
a:hover{text-decoration:underline}

.post{
	background:#eee;
	padding:5px;
	margin:5px;
	border:solid #999;
	border-width:3px 0;
}

Conclusão

Nesse tutorial você deve ter aprendido os seguintes itens:

  1. Relacionamento muitos-para-muitos em bancos de dados relacionais
  2. Chaves estrangeiras em MySQL
  3. Importância das chaves únicas em bancos de dados relacionais
  4. Inserir campos duplicados em MySQL e evitar que erros sejam retornados
  5. Importância de transações (transaction) em bancos de dados relacionais, além de sua utilização com a extensão PDO
  6. Funções anônimas (closures) em PHP 5.3

É possível que tenha aprendido mais que os itens apresentados acima, mas mencionei os pontos que acho mais importantes. Fique à vontade para perguntar, criticar ou sugerir soluções melhores, acho isso fundamental pro aprendizado de todos. Abraços e até a próxima!

Referências

Tags:, ,

47 Respostas para “Sistema de Tags em PHP e MySQL usando PDO, Transactions, Chaves Estrangeiras e Lambda Functions”

  1. David CHC 25/12/2010 às 11:06 #

    Ótimo Tutorial Eduardo, creio q dava para simplificar mais ainda algumas coisas, mas dependendo do público q vc queira atingir, pode complicar. Por exemplo, encapsular SQL em funções, como para exibir todos itens e um item único:

    http://pastebin.com/kVKKUjCQ

    Outro lugar q poderia simplificar (eu particularmente não gosto de acrescentar vírgula, e depois tirar com substr, prefiro trabalhar com array e usar o implode, mais vai de cada um). Na hora de preparar as tags, e assim pegar os ID’s para inserir na tabela MxN, exemplo

    http://pastebin.com/RanMANTs

    Mas sei q didaticamente poderia complicar, ou mesmo assustar, rs.

    Parabéns , e um Feliz Natal.

    abrs

    • Eduardo de Matos 25/12/2010 às 11:25 #

      Certamente poderia simplificar, mas eu quis evitar a criação de funções, pra focar na lógica, que em algumas partes nem é tão simples.

      Eu separei as tags por vírgulas (poderia ser outro separador, como um espaço em branco) porque isso vem se tornando bastante comum, como no WordPress onde você separa as tags por vírgula, ou no Delicious que você separa as tags por um espaço em branco.
      Concordo que array/implode é melhor nessa situação, e vendo melhor meu script acho que essa solução seria mais simples.

      Abraços

  2. David CHC 25/12/2010 às 11:55 #

    No caso da vírgula, estou dizendo na hora de montar o SQL, eu prefiro usar o array/implode, mas para o usuário definir, realmente é melhor colocar esse delimitador como vírgula, fica mais fácil para o usuário.

  3. Marcio Vinicius 28/12/2010 às 16:04 #

    Parabéns Eduardo, ótimo tutorial. Ótimo mesmo! Um nivel muito bom, um pouco complicado em alguns pontos e eu acabei me perdendo, porém refiz com mais calma depois, descansado e com a mente vazia, rs E olha, show de bola! Seu blog está em ótimo nivel para estudo cara, continue assim!

    Parabéns!
    Uma dica: “O que achas de criar um tutorial focando na validação de diversos tipos de dados? Ex: URL”

    Abraços e um ótimo 2011 para todos nós!

    • Eduardo de Matos 28/12/2010 às 18:11 #

      Se puder mencionar os pontos que ficou perdido eu ficaria muito agradecido, porque assim fico sabendo onde posso melhorar minha explicação.

      Eu diria que o importante desse tutorial nem é o resultado, e sim os conceitos. Se você conseguiu captar todos, então já está de bom tamanho!

      Esses dias estudei um bocado o funcionamento dos componentes de filtragem e validação do Zend Framework, e acredito que daria um bom tutorial. Só não sei se falo sobre alguns componentes independentemente ou se falo do framework como um todo, ainda estou pra decidir.

      Abraços

  4. Felipe Girotti 20/01/2011 às 22:30 #

    Parabéns mesmo Eduardo, muito bem feito este tuto, pude aprender mais sobre as FOREIGN KEYS e o GROUP_CONCAT não sabia que existia, facilita demais não tem que fazer mais uma consulta pra retornar os dados.

  5. Vitor 04/02/2011 às 23:38 #

    Parabens Otimo Codigo, Funciona direitinho e mutio bem explicado!
    Tutorial nota 10!!

  6. Vitor 04/02/2011 às 23:41 #

    Uma Duvida, porque as tags não estao com links? Como eu faço para dexar as tags com um link q qdo o usario clicar redirecione ele paraum categoria q ele clicou, tipo se ele clicou em Php vá para outra pag q estao tdos os posts relacionado com php?

    • Eduardo de Matos 05/02/2011 às 11:36 #

      Pra transformar as tags em links você precisa de duas coisas:
      A primeira é percorrer todas as tagse inserir o link em volta delas. A segunda é criar um query string para que possa buscar os posts pelo nome da tag.

      Pra transformar as tags e, links você pode criar uma pequena função

      function converter_tags_em_links($tags)
      {
          $tags_array = explode(',',$tags);
          $tags_array = array_map(function($tag){
      		$t = trim($tag);
              return '&lt;a href=&quot;http://localhost/tc-blog/principal.php?tag='.$t.'&gt;'.$t.'&lt;/a&gt;';
          },$tags_array);
      
      	return implode(', ', $tags_array);
      }
      
      $tags_links = converter_tags_em_links($tags);
      

      No arquivo principal.php você tem que buscar pela existência da tag no query string, e caso ela exista você insere uma cláusula WHERE no SQL.

      	$tag_where = '';
      	if(filter_has_var(INPUT_GET,'tag'))
      	{
      		$tag_where = ' WHERE nome='.$db-&gt;quote(filter_input(INPUT_GET,'tag')). ' ';
      	}
      
      	$sql = &lt;&lt;&lt;SQL
      		SELECT p.id, p.titulo, p.data_criacao, p.conteudo, group_concat( DISTINCT t.nome SEPARATOR ', ') AS tags
      		FROM posts p
      		LEFT JOIN posts_tags pt ON p.id = pt.post_id
      		LEFT JOIN tags t ON t.id = pt.tag_id
      		{$tag_where}
      		GROUP BY p.id
      		ORDER BY p.data_criacao DESC
      SQL;
      

      Cuidado! É altamente recomendável que você use prepared statements, ou poderá sofrer ataques por SQL Injection!

      Qualquer dúvida, estou à disposição.

      Abraços

  7. Marcio Vinicius 17/02/2011 às 14:41 #

    Como de costume eu sempre revejo os tutoriais, para aprender mais e compreender ainda melhor aquilo que o autor quis passar.

    E dessa não não foi diferente! rs
    Eu adorei o tipo UNIQUE em alguns campos na tabela, realmente muito útil e essencial para evitar duplo cadastro!

    Parabéns novamente!

  8. Marcio Vinicius 18/02/2011 às 14:54 #

    Eduardo, estou eu aqui novamente…. Na “Criar Tag” você usou o bloco try/catch para verificar se a tag já existe e assim retorna a mensagem para o usuário.

    Então eu pesquisei pelo método getCode() e pude entender que ele retorna o valor númerico daquela exceção. Porém não entendi porque você utilizou o número 23000 ( $e->getCode() == 23000 ).

    De onde você tirou esse valor?

    Abraços

    • Eduardo de Matos 18/02/2011 às 17:15 #

      Eu fiz um teste pra ver qual número era retornado nesse tipo de erro, daí constatei que era o número 23000 e usei no bloco condicional.
      Deve existir uma tabela em algum lugar da internet com os códigos de todos os erros, mas achei mais rápido testar.

    • Marcio Vinicius 21/02/2011 às 9:33 #

      Huuum, entendi. Eu procurei por uma tabela mas não encontrei nada.

      Testar é uma solução mais rápida mesmo.
      Obrigado.

  9. ruben_franca 01/03/2011 às 17:52 #

    Nao li todo o material por pressa, lerei amanha, já vi que será util para mim, Mas antecipando, ficou uma duvida pq a necessidade uma tabela extra (post-tags), o relacionamento nao poderia ser direto ? Posts se relacionando com Tags e vice-versa ?
    Digamos que o nosso banco tenha 8 tabelas e ae ?

    Obrigado.

    • Eduardo de Matos 02/03/2011 às 20:26 #

      Esse é um caso típico de relacionamento “muitos pra muitos”, e esse tipo de relacionamento exige uma terceira tabela que vai funcionar como ponte entre as outras duas. Como exercício você pode tentar criar esse relacionamento usando somente duas tabelas (uma de posts e outra de tags), e logo vai ver que não é possível sem que haja duplicação de linhas.

      Procurei um tutorial em português sobre esse assunto, mas não encontrei. Então busquei em inglês e achei esse. Espero que consiga entender!

      Não entendi a parte das “8 tabelas”. Pode detalhar?

      Abraço

    • ruben_franca 04/03/2011 às 9:04 #

      Sem problema, vou seguir o link.

      Detalhando:
      Um banco com 8 tabelas, vamos dar nomes: Tb_cad01, Tb_cad02, _Tb_cad03, Tb_cad04, Tb_cad05, Tb_cad06, Tb_cad07, Tb_cad08

      o relacionamento é 1:n entre
      Tb_cad01 com todas as tabelas
      e Tb_cad02 com as Tb_cad03, Tb_cad04, Tb_cad05

      É só um exemplo, nesse caso como faremos, usaremos tabelas extras e colaremos chaves fz como pontes? Ou ligaremos direto?

      • Eduardo de Matos 04/03/2011 às 13:36 #

        Só é correto usar uma tabela de ponte quando o relacionamento é n:n.

        Quando o relacionamento é 1:n (ou n:1, que é a mesma coisa, mas que olhando pelo outro lado), então nao há necessidade de criação de uma nova tabela, e a ligação geralmente se faz através de uma chave estrangeira.
        Vou dar um exemplo. Vamos supor que você esteja criando um blog. Cada post desse blog só pode ter um autor, mas um autor pode criar vários posts. Esse é um caso típico de relacionamento 1:n (autor:post). Nessa modelagem você criaria uma tabela para usuários e outra para posts. A tabela de posts teria de ter um campo chamado id_usuario por exemplo, onde esse campo carregaria uma referencia ao usuário que criou o post.

        No relacionamento 1:1 a coisa também acontece assim.

        Abraço

  10. Claiton Neisse 01/03/2011 às 23:01 #

    Para funcionar o transformar tags em links deve ser ” $tag_where = ‘ WHERE nome=’.$db->quote ” e nao “$tag_where = ‘ WHERE tag=’.$db->quote” pois na tabela tags o nome da coluna e “nome” e nao “tag”

    Abraço.

  11. ruben_franca 04/03/2011 às 17:21 #

    Quase lá, pra fechar esse papo me explica outra duvida, (se é q ainda posso abusar da sua paciencia, ehehehe) mexendo na minhas tabelas noto que nao consigo auto-incrementar um chave estrangeira nem colocar como indice, é isso mesmo ? Entao que conteudo ela pode receber? Esse conteudo eu mesmo que coloco?
    Conhece outro link de referencia ae q possa passar sobre Modelos de relacionamento e como trabalha-los no mysql e tratar no php ?

    • Eduardo de Matos 04/03/2011 às 20:18 #

      1º – Não faz sentido auto-incrementar uma chave estrangeira, já que ela é só uma referência a uma entidade de outra tabela (na tabela de origem ela é uma chave primária, então lá ela é auto-incrementada!).

      2º – Todas as chaves estrangeiras não só podem como DEVEM ser indexadas! Isso por questão de performance, não que o aplicativo deixe de funcionar se você não indexa-las.

      Não confunda chaves primárias com Índices ou com auto-increment. Esses conceitos não são triviais de entender somente na prática, é preciso algum conhecimento teórico. Posso te recomendar o livro Beginning Database Design. O conteúdo é um pouco pesado, mas os capítulos 3 e 4 devem te dar uma base legal sobre o básico de modelagem de bancos de dados.
      A modelagem de banco de dados é agnóstica em relação ao sistema, e isso é bom porque os conceitos são os mesmos independente da linguagem que for usar!

      Fique a vontade pra fazer perguntas, se a resposta tiver de ser complicada eu te passo outros tutoriais ou livros ou cursos como referência, sem stress.

      Abraço

  12. ruben_franca 05/03/2011 às 9:26 #

    O.K. parece ser boa referencia, mas prefiro comprar material em sites brasileiros, tem alguma boa fonte ?

    grato :)

  13. ruben_franca 05/03/2011 às 11:13 #

    Achei um conteudo que nem sabia que existia, tá servindo tb , vou colocar a ligação pra quiser dar uma lida :)

    http://pt.wikibooks.org/wiki/SQL/Banco_de_dados_Relacional

    Ou simplesmente, digitem Banco de dados no Wiki

  14. Marcio Vinicius 17/03/2011 às 17:25 #

    Eduardo, gostaria de lhe fazer uma pergunta voltada apenas para MySQL.

    Quais fatores levam a um rompimento de determinada tabela?

    É uma dúvida minha e eu não encontrei nada relacionado a isso.

    Abraços.

  15. Marcio Vinicius 17/03/2011 às 21:50 #

    Uma tabela corrompida, pode se dizer. Como se fosse um arquivo corrompido, que foi causado por algum tipo de virus, ou algo do tipo.

    E nessa tabela corrompida não irei funcionar direito, como por exemplo listar todos os usuários, ou fazer atualizações, etc.

    • Eduardo de Matos 17/03/2011 às 22:40 #

      Não sou expert quando o assunto é integridade de dados, mas posso te sugerir duas coisas: Valide os dados de entrada e tenha um backup!

  16. David Borges 09/04/2011 às 12:17 #

    cara… mtu bem explicado…parabéns!

    uso ASP+MySql mas estava justamente em busca da lógica da gestão das tags…me ajudou bastante..

    salvo nos meus favoritos.

    obrigado por compartilhar!

    abçs

  17. Thiago 13/06/2011 às 17:34 #

    Eduardo, estava desenvolvendo um sistema de posts e tags em nossa sistema, até agora tudo tranquilo, só que não entendi direito a parte de linkar (separadamente) cada tag, se poder me ajudar fico grato, Obrigado.. abraços

    • Eduardo de Matos 14/06/2011 às 6:46 #

      Isso depende de como você vai ter o retorno do banco de dados. Mas a ideia básica é obter um array de tags, daí você transforma cada elemento em um link.

      Supondo que você obtenha as tags como uma string:

      Código

      Se você já tiver as tags como array, basta você pular as duas primeiras linhas do código acima.
      A função array_map serve para transformar cada elemento de um array (nesse caso transformei os nomes em links html).

      Em poucas palavras: transforme crie 1 link pra cada tag, nem que você tenha que usar um loop ‘foreach’ pra isso.

  18. Thiago 14/06/2011 às 15:20 #

    Opa, voltei, funcionou amigão, depois de muita persistência, haha, ótimo tutorial.
    E valeu por tirar minha duvida, depois que o sistema estiver no finalizado dou uma passadinha aqui pra você ver como ficou, ta ficando show. conectare.com br vem ai

    o codigo ficou assim

    getPostTags();
    $tags_array = explode(“,”, $var);
    $links = array_map(function($param) {
    echo’‘. $param .’,‘;
    }, $tags_array);
    ?>

  19. Thiago 14/06/2011 às 15:24 #

    Opa, voltei, funcionou amigão, depois de muita persistência, haha, ótimo tutorial.
    E valeu por tirar minha duvida, depois que o sistema estiver no finalizado dou uma passadinha aqui pra você ver como ficou, ta ficando show. conectare.com br vem ai

    o codigo ficou assim

    getPostTags();
    $tags_array = explode(“,”, $var);
    $links = array_map(function($param) {
    echo’‘. $param .’,‘;
    }, $tags_array);
    ?>
    getPostTags();
    $tags_array = explode(“,”, $var);
    $links = array_map(function($param) {
    echo’‘. $param .’‘;
    }, $tags_array);
    ?>

  20. André 04/07/2011 às 18:17 #

    E ae cara… tranquilo!

    Primeiro parabéns pelo blog e pelo excelentes tutoriais…

    Estou com uma dúvida que não se restringe especificamente ao sistema criado acima, mas sim a respeito de um artifício usado.

    O join nas consultas SQL…

    Eu faria da seguinte maneira – justamente por não entender o uso do join

    SELECT posts.id, posts.titulo, posts.data_criacao, posts.conteudo, group_concat( DISTINCT tags.nome SEPARATOR ‘, ’) AS tags
    FROM posts, tags. posts_tags
    WHERE posts_tags.post_id = posts.id AND
              posts_tags.post_tag = tags.id
    GROUP BY posts.titulo
    ORDER BY data_criacao DESC
    

    Mas então, o que difere uma maneira da outra (se é que a minha daria certo)?

    • Eduardo de Matos 04/07/2011 às 19:34 #

      O que você fez no seu SQL não foi nada mais nada menos que um inner join.

      FROM a , b
      WHERE a.campo1 = b.campo2
      

      É rigorosamente o mesmo que

      FROM a INNER JOIN b ON a.campo1 = b.campo2
      

      Somente a sintaxe muda de um pro outro.
      Só não esqueça que existem outros tipo de join, como LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN
      é importante você verificar a compatibilidade deles no banco de dados que for usar, porque não é garantido que todos os tipos funcionem em todos os bancos (o FULL OUTER JOIN não funciona no MySQL por exemplo).

      Abraço!

    • André 04/07/2011 às 22:56 #

      Vlw pela explicação cara… E parabéns mais uma vez pelo ótimo material!

      Abraço

  21. Perdido 02/08/2011 às 21:15 #

    Como faço para filtrar os posts com mais de uma tag? Do jeito que esta consigo filtrar todos os posts com “php” e todos com “mysql”, mas não os posts com “php” e “mysql”, por exemplo.

    • Eduardo de Matos 03/08/2011 às 7:18 #

      Eu não sei como você daria essa opção pro usuário, mas a busca no banco de dados é bem simples. Basta você usar a cláusula OR:

      WHERE tags.nome = 'php' OR tags.nome = 'mysql'
      
      • Victor Martinez 21/02/2012 às 20:17 #

        Eduardo, primeiramente parabéns pelo tópico. Muito bem detalhado . Sou iniciante na área de desenvolvimento web e estou fazendo o sistema de tags para meu blog.

        Estou com um problema parecido com o do Perdido:

        Eu quero buscar no banco os posts que contém a tag ‘php’. Um post pode ter a tag php e outras tags associadas. Contudo, quando busco no banco através do SELECT ele só apresenta, na área das tags, a tag php e não as outras tags.

        Como resolver isso?

      • Eduardo de Matos 21/02/2012 às 21:03 #

        @Victor, você pode fazer isso de duas maneiras. A primeira é executando uma nova SQL para cada registro encontrado na busca por tags (se existem 10 posts com uma tag que você buscou, então você faria uma nova consulta ao banco de dados para cada um desses resultados, a fim de saber quais tags esse post tem).

        Outra solução é encapsular essa lógica em uma SQL. A ideia é dividir a SQL em duas partes. A primeira parte busca o ID dos posts que tem uma dada tag (a tag que você está buscando). A segunda parte consiste em buscar as tags dos posts encontrados na primeira parte.

        Segue o resultado:

        SELECT p.id, p.titulo, group_concat(DISTINCT t.nome SEPARATOR ', ') AS tags FROM posts p
        	INNER JOIN posts_tags pt ON p.id = pt.post_id
        	INNER JOIN tags t ON t.id = pt.tag_id
        	WHERE p.id IN
        	(
        		SELECT p.id
        			FROM posts p
        			LEFT JOIN posts_tags pt ON p.id = pt.post_id
        			LEFT JOIN tags t ON t.id = pt.tag_id
        			WHERE t.name = :nome_tag
        	)
        GROUP BY p.id;
        

        Obs.: Você deve substituir :nome_tag pelo valor passado pelo usuário na URL (idealmente usando prepared statement).

      • Victor Martinez 22/02/2012 às 12:22 #

        Infelizmente não funcionou. Estou desde ontem me batendo para tentar resolver esse problema. Tentei até outros comandos. Revisei o código, entendi a lógica mas sempre aparece “mysql_fetch_array(): supplied argument is not a valid MySQL result”. Enfim, por questão de performance não queria ter que aplicar 1 consulta SQL para cada resultado. Mas, estou vendo que vai ser o jeito. Muito obrigado pela ajuda Eduardo.

      • Eduardo de Matos 22/02/2012 às 19:52 #

        Tenta colocar o nome de todas as tabelas entre o simbolo `.
        Por exemplo:

        SELECT * FROM `tags` WHERE `tag`.`nome` ...
        
      • Victor Martinez 24/02/2012 às 12:41 #

        Já tentei também e nada. Mais uma vez, muito obrigado pela atenção.

  22. Hiro 04/08/2011 às 16:29 #

    Oi, não entendi muito bem sua explicação de transformar as tags em links, poderia detalhar um pouco mais?

    eu colocaria esse código

    function converter_tags_em_links($tags)
    {
    $tags_array = explode(‘,’,$tags);
    $tags_array = array_map(function($tag){
    $t = trim($tag);
    return ‘<a href="‘.$t.’‘” rel=”nofollow”>http://localhost/tc-blog/principal.php?tag=‘.$t.’>’.$t.’‘;
    },$tags_array);

    return implode(‘, ‘, $tags_array);
    }

    $tags_links = converter_tags_em_links($tags);

    no lugar deste?

    $tags = array_map(function($value){
    return trim($value);
    }, explode(‘,’, filter_input(INPUT_POST, ‘tags’)));

    $tags_links = converter_tags_em_links($tags);

    • Eduardo de Matos 04/08/2011 às 16:57 #

      Fazem a mesma coisa. Só estão escritos de forma diferente.

    • hiro 06/08/2011 às 20:45 #

      Na verdade eu consegui resolver meu problema de um jeito mais simples.

      $tags = array_map(function($value){
      $t = trim($value);
      return ‘‘.$t.’‘;
      }, explode(‘,’, filter_input(INPUT_POST, ‘tags’)));

      ou pelo menos acho que resolvi, ocorre algum problema se eu fizer assim?

    • Eduardo de Matos 06/08/2011 às 21:36 #

      Aparentemente nenhum problema.

      Só toma cuidado com o nome da variável. Nesse caso eu colocaria um nome diferente, já que essa função não retorna as tags, e sim os links que direcionam pras tags.

      Abraço.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Sair / Alterar )

Imagem do Twitter

You are commenting using your Twitter account. Sair / Alterar )

Foto do Facebook

You are commenting using your Facebook account. Sair / Alterar )

Connecting to %s

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.