Poesía Binaria

Añadir jerarquía a nuestras colecciones de datos en PHP. Creando árboles en PHP

Es una práctica muy común categorizar nuestros. Hacer que existan categorías/sub-categorías/sub-sub-categorías sin límite al igual que directorios o carpetas hay en nuestro ordenador necesitamos tener todo clasificado. Es decir, queremos introducir jerarquía en nuestros datos, hacer que unos campos dependan de otros.
Pero cuando guardamos la información en base de datos, normalmente se guardarán en una tabla con dos dimensiones, por lo que perdemos esa sensación de que hay unos datos dentro de otros. Lo normal en estos casos, es que en cada fila de nuestra tabla haya un campo extra que apunta al identificador de la entrada que establecemos como padre.

Por ejemplo, si intentamos introducir categorías de pintura, tendremos por ejemplo:

La información que tendremos en nuestra tabla de base de datos será:

IDNombre
1Género
2Técnica
3Soporte
4Histórica
5Retrato
6Paisaje
7Bodegón
8Óleo
9Cera
10Acuarela
11Lienzo
12Papel
13Vidrio
14Prehistoria
15Edad antigua
16Edad media
17Edad moderna
18Edad contemporánea

Añadiendo la entrada padre, esto quedará de la siguiente manera:

IDNombrePadre
1Género 0
2Técnica 0
3Soporte 0
4Histórica 1
5Retrato 1
6Paisaje 1
7Bodegón 1
8Óleo 2
9Cera 2
10Acuarela 2
11Lienzo 3
12Papel 3
13Vidrio 3
14Prehistoria 4
15Edad antigua 4
16Edad media 4
17Edad moderna 4
18Edad contemporánea 4

Ahora será fácil saber de dónde cuelga cada subcategoría. Podremos traer de base de datos los elementos pertenecientes al género histórico (id=4) buscando todos los elementos cuyo padre sea 4 y veremos 14, 15, 16, 17 y 18. También sabemos que las categorías raíz (las primeras que veremos) serán las que cuelgan de 0.

Lo bueno de esta técnica, es que la tabla puede ir rellenándose en la dirección que sea, es decir, podremos introducir de manera indiferente géneros, técnicas, etc (en el ejemplo están por orden para entendernos mejor, pero en la realidad esto puede llegar a ser un caos (técnicas, épocas, géneros y soportes entremezclados) ya que será la base de datos la que se encargue de traer los datos asociados en cada momento.

Hacer las consultas es fácil, pero ahora queremos tener en PHP un array con esta información de forma que la podamos manejar fácilmente. Una primera aproximación a la solución sería la siguiente:

Lo que nos daría una bonita función recursiva que debe funcionar muy bien (al menos al principio). El problema está en que cuando tengamos muchas categorías y éstas se estén consultando muchas veces, sobrecargaremos la base de datos de consultas y éstas tardarán un tiempo precioso en terminar.

Si ponemos como ejemplo las categorías de pintura, el algoritmo para crear ese array de PHP donde almacenaremos todas las categorías con su jerarquía sería el siguiente (consultas de base de datos en negrita):

Lo que hace un total de 17 consultas a base de datos. Lo cual es inviable. Debemos de saber que es mucho más rápido pedir una cantidad de datos grande de una vez que hacer 17 peticiones pequeñas (lo segundo casi casi tarda 17 veces más). No es cuestión de pedir datos que no necesitamos, pero si por ejemplo queremos dibujar un árbol con las categorías sí que necesitamos tenerlas todas en memoria por lo que es mucho más rápido pedir una sola vez la tabla completa de subcategorías y trabajarla para obtener nuestro array final.

Para ello, podemos utilizar múltiples algoritmos, aunque uno muy rápido es el siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
$ex = array(
array('id' => 1,
'nombre' => 'Género',
'parentId' => 0),
array('id' => 2,
'nombre' => 'Técnica',
'parentId' => 0),
array('id' => 3,
'nombre' => 'Prehistoria',
'parentId' => 7),
array('id' => 4,
'nombre' => 'Edad antigua',
'parentId' => 7),
array('id' => 7,
'nombre' => 'Histórica',
'parentId' => 1),
array('id' => 5,
'nombre' => 'Óleo',
'parentId' => 2),
array('id' => 6,
'nombre' => 'Retrato',
'parentId' => 1),
array('id' => 8,
'nombre' => 'Soporte',
'parentId' => 0),
array('id' => 9,
'nombre' => 'Lienzo',
'parentId' => 8),
array('id' => 10,
'nombre' => 'Edad media',
'parentId' => 7),
array('id' => 11,
'nombre' => 'Paisaje',
'parentId' => 1),
array('id' => 12,
'nombre' => 'Bodegón',
'parentId' => 1),
array('id' => 13,
'nombre' => 'Cera',
'parentId' => 2),
array('id' => 14,
'nombre' => 'Acuarela',
'parentId' => 2),
array('id' => 15,
'nombre' => 'Papel',
'parentId' => 8),
array('id' => 16,
'nombre' => 'Vidrio',
'parentId' => 8),
array('id' => 17,
'nombre' => 'Edad moderna',
'parentId' => 7),
array('id' => 18,
'nombre' => 'Edad contemporánea',
'parentId' => 7),
);

function buildTree($data, $rootId=0)
{
$tree = array('children' => array(),
'root' => array()
);
foreach ($data as $ndx=>$node)
{
$id = $node['id'];
/* Puede que exista el children creado si los hijos entran antes que el padre */
$node['children'] = (isset($tree['children'][$id]))?$tree['children'][$id]['children']:array();
$tree['children'][$id] = $node;

if ($node['parentId'] == $rootId)
$tree['root'][$id] = &$tree['children'][$id];
else
{
$tree['children'][$node['parentId']]['children'][$id] = &$tree['children'][$id];
}

}
return $tree;
}

print_r(buildTree($ex));

Esto nos creará una estructura con dos índices:

En el primero de ellos se irán almacenando cada uno de los elementos en bruto y en el segundo nuestra tan ansiada estructura en forma de árbol. La esencia de este algoritmo radica en su complejidad O(n) o lo que es lo mismo, realizaremos tantas operaciones como elementos totales haya en nuestra tabla de categorías. Con esto, nos quitamos un algoritmo recursivo (y la sobrecarga que produce) y lo hacemos tan sencillo como pasar datos de un lado a otro.
Lo que estamos haciendo aquí es copiar las referencias de cada uno de los elementos de la lista y no los elementos en sí. De esta forma, si la referencia de un elemento del índice ‘children’ tiene dos elementos hijos dentro de él la copiamos al índice ‘root’, cuando consultemos dentro de root, estaremos viendo los dos hijos; pero si ahora, le añadimos al mismo elemento dentro de children otro hijo, al consultarlo desde ‘root’ veremos este nuevo hijo. Lo mejor es que no se están realizando copias de datos completos, sino de referencias, por lo que, aunque veamos los mismos elementos en ‘root’ y en ‘children’ no están ocupando el doble.

Por lo general, esta función buildTree() admitirá un array de elementos, cada uno de esos elementos deberá ser un array y obligatoriamente, debemos tener una clave id y otra clave parentId (luego podremos hacerlo configurable si queremos). Por defecto al ID de la categoría o elemento raíz será 0.

He de decir también, que en este ejemplo, he revuelto el orden de los elementos, para que veamos que no importa en qué posición estén, es más, podemos incluso especificar los elementos hijos antes que los elementos padre (muy útil cuando queremos que vengan en un orden determinado en base de datos).

Actualización 20/02/2018: He arreglado un & que no salía bien en el código. Además, ¡era la clave del post!

Foto: Robert Couse-Baker (Flickr CC-by)

También podría interesarte....