Hace unos días me vi obligado a crear una estructura de árbol desde una base de datos, y a partir de allí, crear varios menús jerárquicos, mapas de sitio y "breadcrumbs".
A pesar de que aún lo estoy "puliendo", me ha funcionado de maravillas. Así que pensé en compartirlo con ustedes.
A decir verdad, no es algo tan complicado de hacer... Pero yo quise hacer algo "reutilizable", teniendo en cuenta lo siguiente:
- No depender de una conexión a una base de datos;
- tampoco depender del orden en el que los datos se presenten;
- utilizar una sola sentencia SQL para cada árbol;
- crear un árbol "sín límites" de profundidad;
- poder ordenar el árbol una vez construído;
- y obtener datos por su id.
Antes que nada, como yo trabajo con PHP 5.4+, pense hacer una versión del código alternativa, para versiones de PHP anteriores. Pero aclaro: sólo pude probarlo en las siguientes versiones de PHP: 5.4.7 (ambos códigos) y 5.3.24 (sólo la versión por procedimientos), y funcionó a la perfección.
El código contiene de 2 interfaces y 4 clases, en la version orientada a objetos. Y la versión por procedimientos, una sola función.
Ahora, debido a que en el título menciono "desde base de datos", estoy obligado a mostrarles cómo, pero la verdad es que los datos pueden provenir de cualquier otro medio, solo tiene que respetarse la estructura de tabla (es decir, un array donde cada elemento es una fila, o sea, otro array u objeto con los datos) y contener 2 campos obligatorios: un campo con un ID ÚNICO, y otro campo indicando ese mísmo ID ÚNICO, pero del elemento padre.
A continuación les muestro la base de datos que yo estoy utilizando, solo que con los datos necesarios para esta ocación, cada quien lo puede modificar a su gusto.
Código SQL:
Ver original
-- -- Estructura de tabla para la tabla `links` -- CREATE TABLE IF NOT EXISTS `links` ( `id` SMALLINT(5) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(200) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Estructura de tabla para la tabla `links_hierarchy` -- CREATE TABLE IF NOT EXISTS `links_hierarchy` ( `link_id` SMALLINT(5) UNSIGNED NOT NULL, `parent_id` SMALLINT(5) UNSIGNED NOT NULL DEFAULT '0', `group_id` SMALLINT(5) UNSIGNED NOT NULL DEFAULT '0', `order_no` SMALLINT(5) NOT NULL DEFAULT '0', KEY `parent_id` (`parent_id`), KEY `group_id` (`group_id`), KEY `link_id` (`link_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Filtros para la tabla `links_hierarchy` -- ALTER TABLE `links_hierarchy` ADD CONSTRAINT `links_hierarchy_ibfk_1` FOREIGN KEY (`link_id`) REFERENCES `links` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
Ahora, la razón por la que decidí dividirlo en 2 tablas distintas, no es sólo para mantener la base de datos más ordenada. Primero les explico los campos extras:
group_id => Es simplemente para poder agrupar árboles. Como dije antes, necesitaba crear varios árboles, por eso creí que sería útil agrupar los datos de esa forma. Por ejemplo, si quiero crear un árbol para un menú principal, y otro para un submenú, asocio al grupo "0" los elementos del menú principal y al grupo "1" los elementos del submenú. Entonces, a la hora de obtener los datos, los filtro por su grupo, con la cláusula WHERE group_id = 0
order_no => Es simplemente para poder ordenar los elementos de forma manual, asignando un número de orden.
Ahora sí, el motivo por el cual separo los datos en 2 tablas, es porque me permite mostrar 1 elemento en varios grupos, sín la necesidad de copiar todos los datos, además que no se puede teniendo las filas con ID único. Por lo tanto, sólo agregamos una fila en links_hierarchy para ubicar un elemento en otro grupo.
Para obtener los datos, la siguiente consulta me ha funcionado bastante bien:
Código PHP:
Ver original
$sql_query = ' SELECT links.id, links.name, links_hierarchy.group_id, links_hierarchy.parent_id, links_hierarchy.order_no FROM links INNER JOIN links_hierarchy ON links_hierarchy.link_id = links.id WHERE links_hierarchy.group_id = 0 AND links.id > 0 ';
Importante: no creo que alguien lo haga, pero NO utilizar el id 0 en la base de datos, ya que al generar el árbol, como éste es también tratado como un nodo en sí, se le asigna el id "0", al ser la raíz de todo el árbol.
Ahora, empecemos con la versión por procedimientos; es una función que acepta 3 parametros:
Código:
$table => Es la tabla de datos de donde se construirá el árbol. Ejemplo:array table_tree(array $table[, bool $sort = false[, array $options = array()]])
Código PHP:
Ver original
// Normalmente el resultado obtenido de una tabla de una base de datos, // luego de ejecutar mysqli_fetch_assoc() o mysqli_fetch_object(), por ejemplo // ... );
Nota: en caso de que se obtengan las filas con mysqli_fetch_object(), el código forzará la fila hacia un array, sólo para obtener los ids necesarios. El valor del nodo seguirá siendo la fila (objeto) original.
$sort => Indica si queremos ordenar el árbol, o no. Por defecto, es falso.
$options => Indica opciones de entrada/salida de datos. Estos son los valores que se utilizan por defecto:
Código PHP:
Ver original
// Índices de entrada; nombre de los campos en la base de datos // o el alias utilizado en la consulta 'id_key' => 'id', 'parent_id_key' => 'parent_id', // Índices de salida; los primeros tres serán los índices de los datos // en cada nodo, y del árbol resultante en sí; y los otros dos son los // extra que contiene el nodo del árbol; el primero contiene referencias // a todos los nodos en el arbol, por su id; y el segundo, los nodos cuyo // padre no se pudo encontrar 'value_key' => 'value', 'parent_key' => 'parent', 'children_key' => 'children', 'nodes_key' => 'nodes', 'orphans_key' => 'orphans', // Comparador para ordenar los nodos, debe ser de tipo "callable" // como especifíca la función usort(), o un int, para especificar "flags" // como en la función sort() 'comparator' => SORT_REGULAR );
A continuación, les muestro cómo la función construye la estructura de árbol:
Código PHP:
Ver original
// Estructura de un nodo // Puede ser un objeto, depende de como obtenemos los filas $options['parent_key'] => &$parent_node, 0 => $node, 1 => $node, 2 => $node // ... ) ); // Estructura de del árbol final // Los dos primeros, sólo para seguir con la estructura de un nodo, aunque // se le podría agregar un valor manualmente $options['value_key'] => null, $options['parent_key'] => null, 0 => $node, 1 => $node, 2 => $node // ... ), // Mapa de nodos, por referencias ('id' => &$nodo) 'node_id' => &$node, 'node_id' => &$node, 'node_id' => &$node, // ... ), // Los nodos que deberían tener un padre, pero no se encontro en la tabla 0 => $orphan_node, 1 => $orphan_node, 2 => $orphan_node // etc. ), // ... ) );
(Continúa en comentarios)