Menu dinâmico com submenus infinitos utilizando AngularJS e PHP

Neste artigo veremos uma maneira de construir um menu dinâmico, ou seja, no cadastro do menu será definido o menu superior e através de um método recursivo construiremos o menu completo hierarquicamente, permitindo um número “infinito” de submenus.

No lado servidor iremos utilizar PHP para buscar os menus no banco de dados e organizá-los em um vetor de forma hierárquica.

No lado cliente iremos utilizar AngularJS para construir o menu visualmente com base nos dados retornados pelo servidor.

Imagem 1 – Resultado final

O banco de dados

Primeiramente vamos criar uma tabela no banco de dados e inserir alguns menus para teste.

CREATE TABLE `menus` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`menu_id_superior` INT(11) NULL DEFAULT NULL,
	`descricao` VARCHAR(40) NOT NULL,
	PRIMARY KEY (`id`),
	INDEX `FK_MENU_SUPERIOR` (`menu_id_superior`),
	CONSTRAINT `FK_MENU_SUPERIOR` FOREIGN KEY (`menu_id_superior`) REFERENCES `menus` (`id`)
)
ENGINE=InnoDB;

INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (1, NULL, 'Menu 1');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (2, NULL, 'Menu 2');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (3, 1, 'Menu 1.1');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (4, 3, 'Menu 1.1.1');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (5, 4, 'Menu 1.1.1.1');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (6, 1, 'Menu 1.2');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (7, 2, 'Menu 2.1');
INSERT INTO `menus` (`id`, `menu_id_superior`, `descricao`) VALUES (8, 2, 'Menu 2.2');

Uma tabela bem simples; apenas a descrição do menu e o id do menu superior que faz referência à própria tabela.

O lado servidor

Primeiramente, vamos criar um arquivo responsável pela conexão com o banco de dados utilizando PDO.

<?php

// Informações para conexão
$host = 'localhost';
$usuario = 'root';
$senha = '';
$banco = 'rafaelcouto';
$dsn = "mysql:host={$host};port=3306;dbname={$banco}";

try {
    // Conectando
    $pdo = new PDO($dsn, $usuario, $senha);
} catch (PDOException $e) {
    // Se ocorrer algum erro na conexão
    die($e->getMessage());
}

Pronto, agora vamos criar o código para buscar os menus no banco de dados.
<?php

// Incluindo arquivo de conexão com o banco de dados
require_once('config/database.php');

// Selecionando todos os menus
$sql = "SELECT * FROM menus";
$query = $pdo->query($sql);
$menus = $query->fetchAll(PDO::FETCH_ASSOC);

/**
 * Função de apoio para contrução do menu de forma recursiva
 *
 * @param array $menus Vetor contendo todos os menus
 * @param array $menuFinal Vetor contendo o menu final com estrutura hierarquica
 * @param int $menuSuperiorId Id do menu superior para encontrarmos os sub-menus
 * @param int $nivel Nivel de hierarquia do menu
 */
function construirMenu(array $menus, array &$menuFinal, $menuSuperiorId, $nivel = 0)
{
    // Passando por todos os menus
    foreach ($menus as $menu) {
        // Se for um menu filho do menu superior que estamos procurando
        if ($menu['menu_id_superior'] == $menuSuperiorId) {
            // Armazenando no menu final
            $menuFinal[] = $menu;
        }
    }

    // Incrementando nível
    $nivel++;

    // Passando pelos menus desse nível
    for ($i = 0; $i < count($menuFinal); $i++) {

        // Inicializando indices
        $menuFinal[$i]['sub_menus'] = [];
        $menuFinal[$i]['nivel'] = $nivel;

        // Chamando a função novamente para construção dos submenus
        construirMenu($menus, $menuFinal[$i]['sub_menus'], $menuFinal[$i]['id'], $nivel);
    }

}

// Chamada inicial da função
$menuFinal = [];
construirMenu($menus, $menuFinal, null);

// Retornando menu com hierarquia
echo json_encode($menuFinal);

  • Linha 7: Basicamente, temos duas opções aqui; podemos selecionar todos os menus de uma vez ou então selecionar apenas os menus superiores e no processo de construção do menu selecionamos os submenus de cada menu. Neste caso, implementamos a primeira opção, pois executamos apenas uma query no banco de dados, diferente da segunda opção que executaríamos uma query para cada menu e submenu.
  • Linha 19: O único detalhe aqui é que passamos o menuFinal como referência, pois essa será uma função recursiva e não podemos perder o conteúdo desse vetor nas chamadas subsequentes.
  • Linha 22 até 28: Primeiramente, é preciso achar os submenus do menu superior em questão. Portanto, percorremos o vetor contendo todos os menus, verificamos quem é filho dele e então armazenamos no menuFinal.
  • Linha 34 até 42: Agora que temos os submenus, percorremos cada um deles e chamamos a função novamente, porém passando por parâmetro:
    • 1) O vetor contendo todos os menus, pois precisamos dele para procurar o submenus do submenu em questão;
    • 2) O sub-vetor, que inicializamos anteriormente, que irá armazenar os submenus deste submenu;
    • 3) O id do menu superior passa a ser o id do submenu, pois agora queremos encontrar os submenus deste submenu;
    • 4) O nível hierárquico atual;
  • Linha 48: Pronto, agora basta fazermos a chamada inicial da função para gerarmos o vetor com o menu estruturado hierarquicamente. No terceiro parâmetro passamos o valor null, pois devemos partir dos menus superiores, ou seja, que não são filhos de ninguém.
  • Linha 51: O menu final deverá conter a estrutura abaixo para que possamos construir o menu no lado cliente.

[
  {
    "id": "1",
    "menu_id_superior": null,
    "descricao": "Menu 1",
    "sub_menus": [
      {
        "id": "3",
        "menu_id_superior": "1",
        "descricao": "Menu 1.1",
        "sub_menus": [
          {
            "id": "4",
            "menu_id_superior": "3",
            "descricao": "Menu 1.1.1",
            "sub_menus": [
              {
                "id": "5",
                "menu_id_superior": "4",
                "descricao": "Menu 1.1.1.1",
                "sub_menus": [],
                "nivel": 4
              }
            ],
            "nivel": 3
          }
        ],
        "nivel": 2
      },
      {
        "id": "6",
        "menu_id_superior": "1",
        "descricao": "Menu 1.2",
        "sub_menus": [],
        "nivel": 2
      }
    ],
    "nivel": 1
  },
  {
    "id": "2",
    "menu_id_superior": null,
    "descricao": "Menu 2",
    "sub_menus": [
      {
        "id": "7",
        "menu_id_superior": "2",
        "descricao": "Menu 2.1",
        "sub_menus": [],
        "nivel": 2
      },
      {
        "id": "8",
        "menu_id_superior": "2",
        "descricao": "Menu 2.2",
        "sub_menus": [],
        "nivel": 2
      }
    ],
    "nivel": 1
  }
]

O lado cliente

Javascript

// Criando module
var app = angular.module('appModule', []);

// Criando controller
app.controller('menuController', function($http) {

    // Armazenando o objeto do controller para evitar conflitos
    var self = this;

    // Lista de menus
    self.menus = [];

    // Método para carregar os menus
    self.carregarMenus = function() {

        // Fazendo requisição AJAX com o servidor para buscar menus
        $http.get('api/menu.php').then(function(response) {

            // Armazena os menus retornados
            self.menus = response.data;

        })

    };

});

Criamos um controller para nossa página que possui uma propriedade para armazenar os menus e um método para carregar os menus do servidor através de uma requisição AJAX.

HTML

<!-- Definindo module que será utilizado -->
<body ng-app="appModule">

<!-- Definindo controller que será utilizado e carregando os menus -->
<div ng-controller="menuController as ctrl" ng-init="ctrl.carregarMenus()">

    <!-- Template de um único menu -->
    <script type="text/ng-template" id="menu.html">
        <span class="nivel{{menu.nivel}}" ng-bind="menu.descricao"></span>
        <ul ng-if="menu.sub_menus">
            <li ng-repeat="menu in menu.sub_menus" ng-include="'menu.html'"></li>
        </ul>
    </script>

    <!-- Chamada inicial para construção do menu recursivamente -->
    <ul>
        <li ng-repeat="menu in ctrl.menus" ng-include="'menu.html'"></li>
    </ul>

</div>

</body>

  • Linha 5: Através do ng-controller definimos que esta div
    é a raiz do nosso controller e dentro dela poderemos acessar suas propriedades e métodos. O ng-init é sempre chamado na criação do elemento, ou seja, quando a div for criada chamaremos o método carregarMenus() definido no controller.
  • Linha 8: Criamos um template que representa um único menu, pois reutilizaremos essa porção de código para criar o menu completo recursivamente.
  • Linha 9: Definimos o nível do elemento em sua class para que possamos estilizá-lo posteriormente e através do ng-bind vinculamos o elemento com a descrição do menu.
  • Linha 10: O ng-if nos permite definir se um elemento será criado ou não através de uma condição. Neste caso, se o menu não possuir submenus, essa lista não será criada.
  • Linha 11: O ng-repeat nos permite repetir um elemento de acordo com uma collection. No caso, iremos repetir o elemento li para cada submenu deste menu. E dentro desta li iremos carregar este template novamente através no ng-include, porém o menu agora será o submenu, tornando esse template recursivo.
  • Linha 17: Fazemos a chamada inicial do template, que neste caso irá passa pelos menus e desencadear a recurvidade como vimos na linha 11.

CSS

Como definimos o nível de cada menu, você pode estilizar cada nível de uma forma diferente, como no exemplo abaixo.

span.nivel1 {
    color: red;
}

span.nivel2 {
    color: blue;
}

span.nivel3 {
    color: limegreen;
}

span.nivel4 {
    color: orange;
}

Conclusão

Neste artigo vimos uma forma de criar uma estrutura de menus dinâmica através da recursividade tanto no lado servidor como no lado cliente.

No lado servidor utilizamos PHP, porém você poderia utilizar qualquer outra linguagem desde que retornasse a mesma estrutura de JSON, assim você não precisa alterar em nada o lado cliente.

No lado cliente utilizamos o AngularJS que nos permite manter um código mais organizado e de fácil manutenção.

Enfim, espero que este artigo tenha sido útil de alguma forma para você. Até a próxima.

Código fonte disponível em: https://github.com/rafaelcouto/menu-dinamico-com-submenus-infinitos-utilizando-angularjs-e-php/

Referências