Categorias e subcategorias: Exemplo de modelagem

Um erro muito comum em modelagem de dados concerne a sistemas de categorias e subcategorias. Muitos não sabem como modelar o banco de dados, criam diversas tabelas e acabam complicando o que é simples.

Mostrarei um forma muito simples de como armazenar essas informações num banco de dados e como exibi-las na tela, na forma de lista, técnica muito utilizada para construção de menus.


Vamos à modelagem, primeiramente.

Teremos apenas uma tabela. Esta é a estrutura dela:

(usarei MySQL neste artigo, mas a lógica da modelagem independe do SGBD usado)

CREATE TABLE categorias(
	id INT(5) UNSIGNED NOT NULL AUTO_INCREMENT,
	id_pai INT(5) UNSIGNED NOT NULL,
	nome VARCHAR(20) NOT NULL,
	PRIMARY KEY (id)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

Vamos popular a tabela da seguinte forma:

INSERT INTO categorias(id, id_pai, nome) VALUES
(1, 0, 'A Empresa'),
(2, 1, 'Sobre Nós'),
(3, 1, 'Objetivos'),
(4, 3, 'Objetivo dos nossos clientes'),
(5, 0, 'Contato'),
(6, 0, 'Produtos');

Obteremos a seguinte tabela:

mysql> select * from categorias;
+----+--------+----------------------+
| id | id_pai | nome                 |
+----+--------+----------------------+
|  1 |      0 | A Empresa            | 
|  2 |      1 | Sobre Nós           | 
|  3 |      1 | Objetivos            | 
|  4 |      3 | Objetivo dos nossos  | 
|  5 |      0 | Contato              | 
|  6 |      0 | Produtos             | 
+----+--------+----------------------+
6 rows in set (0,00 sec)

Nessa estrutura, temos três seções principais: “A Empresa”, “Contato” e “Produtos”. As categorias “Sobre Nós” e “Objetivos” são subcategorias de “A Empresa”. E “Objetivos dos nossos clientes” é subcategoria de “Objetivos”, que, por sua vez, é subcategoria de “A Empresa”, como citado anteriormente.

Vamos fazer uma seleção dessas informações e colocá-las num arrray, como o exibido abaixo.

// array que conterá as categorias
$cats = array();
 
$mysqli = new mysqli( 'localhost', 'user', 'senha', 'bancoDeDados' );
 
$sql = 'SELECT id, id_pai, nome FROM categorias';
 
$exec = $mysqli->query( $sql ) or exit( $mysqli->error );
 
$i = 1;
while ( $f = $exec->fetch_object() )
{
	$cats[$i]['id'] = $f->id;
	$cats[$i]['id_pai'] = $f->id_pai;
	$cats[$i]['nome'] = $f->nome;
	$i++;
}

Obteremos um array como o exibido abaixo:

$cats[1]['id_pai'] = 0;
$cats[1]['nome'] = 'A Empresa';
$cats[2]['id_pai'] = 1;
$cats[2]['nome'] = 'Sobre Nós';
$cats[3]['id_pai'] = 1;
$cats[3]['nome'] = 'Objetivo';
$cats[4]['id_pai'] = 3;
$cats[4]['nome'] = 'Objetivos dos Nossos Clientes';
$cats[5]['id_pai'] = 0;
$cats[5]['nome'] = 'Contato';
$cats[6]['id_pai'] = 0;
$cats[6]['nome'] = 'Produtos';

Note que o array não começou em zero. Os índices do array são os ID’s das categorias no banco de dados. Se começasse em zero, causaria conflito com o id_pai, que é zero para categorias principais.

Agora postarei uma função simples em PHP que montará o menu completo.

/**
 * Função que monta o menu com as categorias e subcategorias.
 * @param id_pai ID da categoria pai cujas subcategorias serão buscadas.
 * @param ArrayCats Array com as categorias do menu.
*/
function montaMenu( $id_pai, $arrayCats )
{
	// calcula o número de índices do array
	$catsSize = count( $arrayCats );
 
	echo "<ul>";
 
	for ( $i = 1; $i <= $catsSize; $i++ )
	{
		if ( $arrayCats[ $i ]['id_pai'] == $id_pai )
		{
			echo "<li>";
			echo $arrayCats[ $i ]['nome'];
 
			// busca as subcategorias da categoria atual
			montaMenu( $arrayCats[ $i ]['id'], $arrayCats );
 
			echo "</li>";
		}
	}
	echo "</ul>";
}

Você pode usar duas variáveis globais, se quiser: o array das categorias e a variável $catsSize. Isso reduz o processamento, uma vez que a função count() seria chamada apenas uma vez. Porém, para projetos grandes, com diversos menus, essa prática não seria bem-vinda.

Para exibir o menu, basta chamar a função da seguinte forma:

montaMenu( 0, $cats );

Ela começará buscando as categorias principais (id_pai = 0) e depois buscará por cada subcategoria.

 

Aprenda Ainda Mais

15 Dicas, Boas Práticas e Fundamentos do PHP

Conheça Dicas FUNDAMENTAIS para programar em PHP de forma Profissional

Não se considere Programador PHP sem antes ler este guia e adotar estas práticas!

Baixe gratuitamente este guia com 15 Dicas de PHP

The following two tabs change content below.
Graduado em Ciência da Computação, pela Universidade Federal do Paraná (UFPR), é desenvolvedor de software desde 2008, com foco em Desenvolvimento Web com PHP.
  • rodrigo

    Cara, parabens muito interessante mesmo, mas tipo, tem como vc colocar um link pra download do script já pronto, ou entao separar o tutorial por arquivos separados, tipo quem é a index.php sei lá, pq eu sou novato em php, e calhou de eu precisar de criar um GC com categorias e subcategorias até nivel 5, e vi q essa aí é a melhor solução.

  • @rodrigo

    Olá, Rodrigo. O post não tem a intenção de criar um sistema, por isso não há arquivos. O propósito é mostrar a modelagem apenas. Logo, isso pode estar em qualquer página, seja uma index.php ou uma qualquer_coisa.php.

    Como você pode ver, só há duas partes em PHP: a função que gera o menu e o trecho responsável por fazer a seleção. Você pode deixar a função num arquivo somente para ela e dar um include/require nas páginas que usarão a função.

    Abraço,

    Beraldo

  • rodrigo

    Sim eu sei, mas digo, como eu disse sou pouco experiente em php, teria como vc criar pra eu entender melhor isso em arquivos.php ???

  • @rodrigo

    Eu não gosto de colocar script pronto para evitar que usuários apenas copiem e colem, sem entender o código. Tente fazer e poste suas dúvidas, aqui ou em algum fórum, onde há mais pessoas para ajudá-lo. ;)

    Abraço,

    Beraldo

  • rodrigo

    ok, aqui deu erro na linha 6, justamente onde está o “mysqli”

    é assim mesmo? não era pra ser mysql connect?

  • @rodrigo

    Verifique se a extensão MySQLi está habilitada. Ela está disponível somente para PHP 5

  • Rodrigo

    Amigão isso é seguro? pq comigo começou a dar alguns bugs, vc não pode criar um tutorial para criar um portal de notícias com este fundamento?

  • @Rodrigo

    Sim. É seguro. É a melhor modelagem para o caso.

  • Beraldo,
    primeiramente parabens cara vc me deu luz, ou melhor um sol, pois iluminou muito minha mente, tava com um problema nesse negocio de categoria e sub, sempre usei a tabela da forma que demonstrou mas meu problema era ordenar de forma dinamica, mas como mostrou, a função chamando a propria função foi fantastico, tão obvio e não percebi, mas queria deixa uma sugestão de melhoria, vou até postar em meu blog com os devidos créditos a inspiração inicial, seguinte da forma que demonstrou os registros tem que estar ordenados no banco, para que funcione corretamente, mas eu pensei num forma de que não seja necessário que estejam ordenados no banco segue abaixo, o código espero que goste da contribuição.

    class Categorias
    {
    private $org_cat_rows;

    function AgrupaCategorias($arrayCats, $pai_id, $pai_campo, $pk_campo)
    {
    $catsSize = count($arrayCats);

    for($i=0; $iorg_cat_rows, $arrayCats[$i]);

    self::AgrupaCategorias($arrayCats, $arrayCats[$i][$pk_campo], $pai_campo, $pk_campo);
    }
    }
    }
    }

    $categorias = new Categorias();
    $categoriasAgrupadas = $categorias->AgrupaCategorias($Vetor_com_categorias, 0, ‘campo_pai’, ‘campo_primary_key’);

    dessa forma fica tudo OK!!!

    Abraços,

    John Marques

  • @John Marques

    Olá.

    De fato, já haviam me informado sobre isso. Eu esqueci de editar o post.

    Valeu por avisar. Já farei a modificação

  • Opa co

    John Marques :Beraldo,primeiramente parabens cara vc me deu luz, ou melhor um sol, pois iluminou muito minha mente, tava com um problema nesse negocio de categoria e sub, sempre usei a tabela da forma que demonstrou mas meu problema era ordenar de forma dinamica, mas como mostrou, a função chamando a propria função foi fantastico, tão obvio e não percebi, mas queria deixa uma sugestão de melhoria, vou até postar em meu blog com os devidos créditos a inspiração inicial, seguinte da forma que demonstrou os registros tem que estar ordenados no banco, para que funcione corretamente, mas eu pensei num forma de que não seja necessário que estejam ordenados no banco segue abaixo, o código espero que goste da contribuição.
    class Categorias{private $org_cat_rows;
    function AgrupaCategorias($arrayCats, $pai_id, $pai_campo, $pk_campo){$catsSize = count($arrayCats);
    for($i=0; $iorg_cat_rows, $arrayCats[$i]);
    self::AgrupaCategorias($arrayCats, $arrayCats[$i][$pk_campo], $pai_campo, $pk_campo);}}}}
    $categorias = new Categorias();$categoriasAgrupadas = $categorias->AgrupaCategorias($Vetor_com_categorias, 0, ‘campo_pai’, ‘campo_primary_key’);
    dessa forma fica tudo OK!!!
    Abraços,
    John Marques

    corrigir o Metodo faltou o array_push para inserir no array e metodo que retorna o membro

    entao ta ai a classe corrigida

    class Categorias
    {
    private $org_cat_rows;

    public function getCategorias()
    {
    return $this->org_cat_rows;
    }

    function AgrupaCategorias($arrayCats, $pai_id, $pai_campo, $pk_campo)
    {
    $catsSize = count($arrayCats);

    for($i=0; $iorg_cat_rows, $arrayCats[$i]);

    self::AgrupaCategorias($arrayCats, $arrayCats[$i][$pk_campo], $pai_campo, $pk_campo);
    }
    }
    }
    }

  • guilherme

    Olá Beraldo, como vai?
    Primeiramente parabéns pela inciativa…
    Eu tentei fazer o que você coloco ai…ele não da nenhum erro, mas também não mostra nada..rs
    Fica minha pagina toda em branco…você tem alguma ideia do que pode estar acontecendo?
    Achei que poderia ser algom com o mysqli mas vi aqui que esta habilitado ja…

    ;extension=php_mssql.dll
    extension=php_mysql.dll
    extension=php_mysqli.dll
    ;extension=php_oci8.dll

    Preciso muito disso para um projeto..que vai ser um catalogo com categorias de produtos e subcategorias.

    Desde ja agradeço

  • @guilherme

    Verifique se você está com as mensagens de erro habilitadas.

    Para habilitá-las, coloque isto no topo do script:

    ini_set( ‘display_errors’, 1 );
    error_reporting( E_ALL );

  • guilherme

    Olá Beraldo,
    fiz o que você pediu e continua tudo branco..rs

  • Vinicius

    Tem algum exemplo de como popular um menu list utilizando algo parecido com esse código sem utilizar estrutura de dados??? Obrigado!

  • @Vinicius

    Não entendi… como irá montar um menu sem ter os dados armazenados em algum lugar? De onde os dados virão?

  • Vinicius

    Os dadoes estão no banco, essa rotina que você criou é uma rotina de exibição de menu de categorias, por exemplo, no caso que eu falo de popular um menu list, eu falo de uma rotina para cadastrar essas categorias, por exemplo, um campo onde a pessoa informa o nome da categoria, e um menu list onde ele escolhe uma categoria pai, caso essa seja uma sub categoria, ou uma sub da sub, o que eu quiz dizer é um menu list que mostra as categorias sem que seja com marcação, por exemplo:

    isso dentro de um meu list

    Categoria 1
    Categoria 2
    Categoria 2 -> Sub Categoria 1
    Categoria 2 -> Sub Categoria 2 -> Sub Categoria 1
    Categoria 3
    categoria 4

    e assim por diante. Obrigado!

  • Vinicius

    Fiz uns testes aqui e deu certo, utilizando recursividade mesmo, se alguém precisar dá um toque que eu escrevo aqui, obrigado!

  • parabéns, excelente post.

  • Carlos Braga

    Ola Beraldo!

    Existe alguma maneira de usar div ao inves de ul ?
    Pergunto pois ja tentei de diversas formas, e ele nao fecha o laco no final

    Estou tentando montar uma listagem de produtos, com seu artigo, ficou bem mais pratico mesmo!

    Valew, Obrigado!

    • Olá, Carlos Braga.

      É possível usar div, sim. Basta prestar atenção à estilização CSS.

  • Guilherme Peixoto

    Como seria um formulário para cadastrar as categorias e sub?

    • Explique melhor sua dúvida.

      Basicamente, basta um formulário com o nome da categoria e um

  • Guilherme Peixoto

    eu postei ela no imasters, só que não tive resposta ainda, se puder me orientar por lá, ou por aqui mesmo, fico agradecido, parabens pela a modelagem.

    AQUI

  • Marcos Arantes

    Só tem um problema: Isso é bom apenas para menus. Para evitar a redundância de dados tem que criar mais 2 tabelas. Tabelas: categorias(idcategoria,categoria)

    subcategorias(idsubcategoria,subcategoria)

    categorias_has_subcategorias(id,categorias_idcategoria,subcategorias_idsubcategoria)

    No meu caso tenho que usar assim para evitar redundância de dados.

    • Não acho que precise de mais tabelas. Uma tabela é suficiente para criar inúmeras subcategorias.

      Não entendi onde há redundância na minha modelagem. Pode me dar um exemplo? Talvez eu não tenha percebido essa falha.

  • Cara, bem legal essa forma de montar categorias, agora eu queria saber o seguinte. Como eu faço um SELECT retornar o nome da categoria como um campo adicional referenciado pelo id_pai. Como se fosse num INNER JOIN, pois os 2 estão na mesma tabela, fiquei meio perdido, será que pode me ajudar?

    • funcionaria somente com 1 instrução dentro da outra?

  • Rafael Andrade

    Muito bom, eu estava procurando uma pequena solução para essa separação de categorias!

    Parabéns

  • Victor

    Oi Beraldo,
    Estava atras justamente dessa modelagem e fiquei com uma duvida.
    Tenho um sistema de postagens onde no caso tenho 3 tabelas:
    1 – Posts
    2 – Categorias
    3 – Cat_Post

    A terceira tabela seria apenas para vincular o post às categorias com dois campos (id_post e id_cat). Mas estou com um pequeno probleminha depois de cadastrar mais de a subcategoria em um post na hora de mostrar as subcategorias que pertencem a categoria principal que não foram cadastradas no post junto com as que foram cadastradas fica assim:
    http://i46.tinypic.com/2vjzyg2.png

    Não sou programador, mas queria muito aprender como faz. Seria meu primeiro site de notícias feito do 0, sera que você pode me ajudar?

  • Pingback: Categorias e subcategorias: Exemplo de modelagem - Matheus Piscioneri()

  • Saymon De O. Souza

    Só uma pequena otimização na função recursiva montaMenu: na versão do poste, assintótica tende a O( n ^ 2), com mais duas variáveis é possível alcançar O( n ):

    ———————————————————————————————————

    function montaMenu( $id_pai, $arrayCats, $leftMost, $rightMost )
    {
    echo “”;

    for ( $i = $leftMost; $i < $rightMost; $i++ )
    {
    if ( $arrayCats[ $i ]['id_pai'] == $id_pai )
    {
    echo "”, $arrayCats[ $i ][‘nome’],
    $leftMost++;
    montaMenu( $arrayCats[ $i ][‘id’], $arrayCats, $leftMost, $rightMost );
    echo “”;
    }
    }
    echo “”;
    }

    //exemplo de uso: montaMenu( 0, $cats, 0, count( $cats ) );