Foros del Web » Programando para Internet » PHP »

[APORTE] Estructura de árbol desde de base de datos

Estas en el tema de [APORTE] Estructura de árbol desde de base de datos en el foro de PHP en Foros del Web. ¡Hola amigos de Foros del Web! Hace unos días me vi obligado a crear una estructura de árbol desde una base de datos, y a ...
  #1 (permalink)  
Antiguo 27/08/2013, 10:03
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Mensaje [APORTE] Estructura de árbol desde de base de datos

¡Hola amigos de Foros del Web!

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
  1. --
  2. -- Estructura de tabla para la tabla `links`
  3. --
  4.  
  5. CREATE TABLE IF NOT EXISTS `links` (
  6.   `id` SMALLINT(5) UNSIGNED NOT NULL AUTO_INCREMENT,
  7.   `name` VARCHAR(200) NOT NULL DEFAULT '',
  8.   PRIMARY KEY (`id`),
  9.   UNIQUE KEY `name` (`name`)
  10. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
  11.  
  12. --
  13. -- Estructura de tabla para la tabla `links_hierarchy`
  14. --
  15. CREATE TABLE IF NOT EXISTS `links_hierarchy` (
  16.   `link_id` SMALLINT(5) UNSIGNED NOT NULL,
  17.   `parent_id` SMALLINT(5) UNSIGNED NOT NULL DEFAULT '0',
  18.   `group_id` SMALLINT(5) UNSIGNED NOT NULL DEFAULT '0',
  19.   `order_no` SMALLINT(5) NOT NULL DEFAULT '0',
  20.   KEY `parent_id` (`parent_id`),
  21.   KEY `group_id` (`group_id`),
  22.   KEY `link_id` (`link_id`)
  23. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  24.  
  25. --
  26. -- Filtros para la tabla `links_hierarchy`
  27. --
  28. ALTER TABLE `links_hierarchy`
  29.   ADD CONSTRAINT `links_hierarchy_ibfk_1`
  30.   FOREIGN KEY (`link_id`)
  31.   REFERENCES `links` (`id`)
  32.   ON DELETE CASCADE
  33.   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
  1. $sql_query = '
  2.     SELECT
  3.         links.id,
  4.         links.name,
  5.         links_hierarchy.group_id,
  6.         links_hierarchy.parent_id,
  7.         links_hierarchy.order_no
  8.     FROM links
  9.     INNER JOIN links_hierarchy
  10.         ON links_hierarchy.link_id = links.id
  11.     WHERE links_hierarchy.group_id = 0
  12.         AND links.id > 0
  13. ';

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:
array table_tree(array $table[, bool $sort = false[, array $options = array()]])
$table => Es la tabla de datos de donde se construirá el árbol. Ejemplo:
Código PHP:
Ver original
  1. // Normalmente el resultado obtenido de una tabla de una base de datos,
  2. // luego de ejecutar mysqli_fetch_assoc() o mysqli_fetch_object(), por ejemplo
  3. $table = array(
  4.     0 => array('campo1' => 'valor1', 'campo2' => 'valor2', 'campo3' => 'valor3'),
  5.     1 => array('campo1' => 'valor1', 'campo2' => 'valor2', 'campo3' => 'valor3'),
  6.     2 => array('campo1' => 'valor1', 'campo2' => 'valor2', 'campo3' => 'valor3')
  7.     // ...
  8. );

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
  1. $options = array(
  2.     // Índices de entrada; nombre de los campos en la base de datos
  3.     // o el alias utilizado en la consulta
  4.     'id_key'        => 'id',
  5.     'parent_id_key' => 'parent_id',
  6.     // Índices de salida; los primeros tres serán los índices de los datos
  7.     // en cada nodo, y del árbol resultante en sí; y los otros dos son los
  8.     // extra que contiene el nodo del árbol; el primero contiene referencias
  9.     // a todos los nodos en el arbol, por su id; y el segundo, los nodos cuyo
  10.     // padre no se pudo encontrar
  11.     'value_key'     => 'value',
  12.     'parent_key'    => 'parent',
  13.     'children_key'  => 'children',
  14.     'nodes_key'     => 'nodes',
  15.     'orphans_key'   => 'orphans',
  16.     // Comparador para ordenar los nodos, debe ser de tipo "callable"
  17.     // como especifíca la función usort(), o un int, para especificar "flags"
  18.     // como en la función sort()
  19.     'comparator'    => SORT_REGULAR
  20. );

A continuación, les muestro cómo la función construye la estructura de árbol:
Código PHP:
Ver original
  1. // Estructura de un nodo
  2. $node = array(
  3.     // Puede ser un objeto, depende de como obtenemos los filas
  4.     $options['value_key'] => array(/* datos de fila */),
  5.     $options['parent_key'] => &$parent_node,
  6.     $options['children_key'] => array(
  7.         0 => $node,
  8.         1 => $node,
  9.         2 => $node
  10.         // ...
  11.     )
  12. );
  13. // Estructura de del árbol final
  14. $node = array(
  15.     // Los dos primeros, sólo para seguir con la estructura de un nodo, aunque
  16.     // se le podría agregar un valor manualmente
  17.     $options['value_key'] => null,
  18.     $options['parent_key'] => null,
  19.     $options['children_key'] => array(
  20.         0 => $node,
  21.         1 => $node,
  22.         2 => $node
  23.         // ...
  24.     ),
  25.     // Mapa de nodos, por referencias ('id' => &$nodo)
  26.     $options['nodes_key'] => array(
  27.         'node_id' => &$node,
  28.         'node_id' => &$node,
  29.         'node_id' => &$node,
  30.         // ...
  31.     ),
  32.     // Los nodos que deberían tener un padre, pero no se encontro en la tabla
  33.     $options['orphans_key'] => array(
  34.         'parent_node_id' => array(
  35.             0 => $orphan_node,
  36.             1 => $orphan_node,
  37.             2 => $orphan_node
  38.             // etc.
  39.         ),
  40.         'parent_node_id' => array(/* nodos huerfanos */),
  41.         'parent_node_id' => array(/* nodos huerfanos */)
  42.         // ...
  43.     )
  44. );

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #2 (permalink)  
Antiguo 27/08/2013, 10:04
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

La función table_tree():
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  */
  7.  
  8. /**
  9.  * Construye una estructura de árbol desde una tabla, donde cada fila tiene un
  10.  * campo con su id único, y un campo indicando el id de su padre.
  11.  *
  12.  * La estructura de las opciones:
  13.  * <pre>
  14.  * array(
  15.  *     // Claves de entrada
  16.  *     'id_key'        => 'id',
  17.  *     'parent_id_key' => 'parent_id',
  18.  *     // Claves de salida
  19.  *     'value_key'     => 'value',
  20.  *     'parent_key'    => 'parent',
  21.  *     'children_key'  => 'children',
  22.  *     'nodes_key'     => 'nodes',
  23.  *     'orphans_key'   => 'orphans',
  24.  *     // Comparador para ordenar los nodos
  25.  *     'comparator'    => SORT_REGULAR // O un "callable" usado por usort()
  26.  * )
  27.  * </pre>
  28.  *
  29.  * @todo Detectar errores recursivos.
  30.  * @todo Detectar ubicaciones múltiples (en un mísmo grupo).
  31.  *
  32.  * @param array $table La tabla de datos.
  33.  * @param bool $sort Indica si los nodos deben ser ordenados, o no.
  34.  * @param array $options Opciones de entrada y salida.
  35.  * @return array
  36.  */
  37. function table_tree($table, $sort = false, $options = array())
  38. {
  39.     if (!is_array($table) || empty($table)) {
  40.         return array();
  41.     }
  42.    
  43.     // Opciones por defecto
  44.     static $default_options = array(
  45.         'id_key'        => 'id',
  46.         'parent_id_key' => 'parent_id',
  47.         'value_key'     => 'value',
  48.         'parent_key'    => 'parent',
  49.         'children_key'  => 'children',
  50.         'nodes_key'     => 'nodes',
  51.         'orphans_key'   => 'orphans',
  52.         'comparator'    => SORT_REGULAR
  53.     );
  54.    
  55.     // Establecer opciones que no fueron especificadas
  56.     if (empty($options)) {
  57.         $options = $default_options;
  58.     } else {
  59.         foreach ($default_options as $name => $value) {
  60.             if (!isset($options[$name])) {
  61.                 $options[$name] = $value;
  62.             }
  63.         }
  64.     }
  65.    
  66.     // Acceso corto a las claves de entrada
  67.     $id_key = $options['id_key'];
  68.     $parent_id_key = $options['parent_id_key'];
  69.    
  70.     // Acceso corto a las claves de salida
  71.     $value_key = $options['value_key'];
  72.     $parent_key = $options['parent_key'];
  73.     $children_key = $options['children_key'];
  74.     $nodes_key = $options['nodes_key'];
  75.     $orphans_key = $options['orphans_key'];
  76.    
  77.     // El árbol final (sigue la estructura de un nodo, más 2 items)
  78.     $tree = array(
  79.         $value_key    => null,
  80.         $parent_key   => null,
  81.         $children_key => array(),
  82.         $nodes_key    => array(),
  83.         $orphans_key  => array()
  84.     );
  85.    
  86.     // Datos temporales
  87.     $nodes = &$tree[$nodes_key];     // Mapa de nodos (ref. nodos en el árbol)
  88.     $orphans = &$tree[$orphans_key]; // Nodos que no se encontro su padre
  89.     $on_hold = array();              // Lista de nodos en espera de su padre
  90.     $on_hold_children = array();     // Nodos en lista de espera
  91.     $on_hold_children_map = array(); // Mapa de nodos en lista de espera
  92.     $sortable_children = array();    // Nodos que deberían ser ordenados
  93.    
  94.     // Agregar el árbol al mapa de nodos bajo el id "0"
  95.     $nodes[0] = &$tree;
  96.    
  97.     // Construír el árbol
  98.     foreach ($table as $index => $row) {
  99.         // Forzar los datos hacia un array, sólo para obtener los ids
  100.         $row = (array) $row;
  101.         $node_id = $row[$id_key];
  102.         $node_parent_id = $row[$parent_id_key];
  103.        
  104.         // Ignorar filas con id = "0"
  105.         if ((int) $node_id === 0) {
  106.             continue;
  107.         }
  108.        
  109.         // Crear nodo
  110.         $new_node = array(
  111.             $value_key => $table[$index],
  112.             $parent_key => null,
  113.             $children_key => array()
  114.         );
  115.        
  116.         // Indica si el padre del nodo actual está en espera
  117.         $parent_is_on_hold = false;
  118.        
  119.         // ¿Se ha encontrado ya el nodo padre de este nodo?
  120.         if (isset($nodes[$node_parent_id])) {
  121.             $parent = &$nodes[$node_parent_id];
  122.         } elseif (isset($on_hold[$node_parent_id])) {
  123.             $on_hold_key = $on_hold[$node_parent_id];
  124.             $parent = &$on_hold_children_map[$on_hold_key][$node_parent_id];
  125.             $parent_is_on_hold = true;
  126.         } elseif (isset($parent)) {
  127.             unset($parent);
  128.         }
  129.        
  130.         // Dar nodo a su padre (si éste ha sido encontrado)
  131.         if (isset($parent)) {
  132.             $parent_children = &$parent[$children_key];
  133.             $parent_children[] = $new_node;
  134.             $parent_children_count = count($parent_children);
  135.             $node = &$parent_children[$parent_children_count - 1];
  136.            
  137.             // Establecer el nodo padre
  138.             $node[$parent_key] = &$parent;
  139.            
  140.             if ($parent_is_on_hold) {
  141.                 // Agregar nodo al mapa de nodos en espera
  142.                 $on_hold[$node_id] = $on_hold[$node_parent_id];
  143.                 $on_hold_children_map[$on_hold[$node_id]][$node_id] = &$node;
  144.             } else {
  145.                 // Agregar nodo al mapa final
  146.                 $nodes[$node_id] = &$node;
  147.             }
  148.            
  149.             // Agregar nodos hijos para ordenar, sólo si es necesario
  150.             if ($sort && $parent_children_count > 1
  151.                     && !isset($sortable_children[$node_parent_id])) {
  152.                 $sortable_children[$node_parent_id] = &$parent_children;
  153.             }
  154.         } else {
  155.             // Agregar nodo a la lista de espera, hasta que aparezca su padre
  156.             $on_hold[$node_id] = $node_parent_id;
  157.            
  158.             if (!isset($on_hold_children[$node_parent_id])) {
  159.                 $on_hold_children[$node_parent_id] = array();
  160.             }
  161.            
  162.             // Poner el nodo en espera
  163.             $on_hold_children[$node_parent_id][] = $new_node;
  164.             $node = &$on_hold_children[$node_parent_id]
  165.                 [count($on_hold_children[$node_parent_id]) - 1];
  166.            
  167.             if (!isset($on_hold_children_map[$node_parent_id])) {
  168.                 $on_hold_children_map[$node_parent_id] = array();
  169.             }
  170.            
  171.             // Agregar nodo al mapa de lista de espera
  172.             $on_hold_children_map[$node_parent_id][$node_id] = &$node;
  173.         }
  174.        
  175.         // ¿És este nodo el padre de alguno de los nodos en espera?
  176.         if (isset($on_hold_children[$node_id])) {
  177.             // Agregar nodo al árbol final, actualizar referencias, y establecer
  178.             // su nodo padre (manipular 1 nodo manualmente)
  179.             if (count($on_hold_children[$node_id]) < 2) {
  180.                 $on_hold_node = &$on_hold_children[$node_id][0];
  181.                 $on_hold_node[$parent_key] = &$node;
  182.                 $node[$children_key][] = $on_hold_node;
  183.                 $on_hold_node_value = (array) $on_hold_node[$value_key];
  184.                 $on_hold_node_id = $on_hold_node_value[$id_key];
  185.                 $on_hold_children_map[$node_id][$on_hold_node_id]
  186.                     = &$node[$children_key][count($node[$children_key]) - 1];
  187.             } else {
  188.                 foreach ($on_hold_children[$node_id] as &$on_hold_node) {
  189.                     $on_hold_node[$parent_key] = &$node;
  190.                     $node[$children_key][] = $on_hold_node;
  191.                     $on_hold_node_value = (array) $on_hold_node[$value_key];
  192.                     $on_hold_node_id = $on_hold_node_value[$id_key];
  193.                     $on_hold_children_map[$node_id][$on_hold_node_id]
  194.                         = &$node[$children_key]
  195.                             [count($node[$children_key]) - 1];
  196.                 }
  197.             }
  198.            
  199.             // Agregar nodos al mapa final
  200.             $tree[$nodes_key]+= $on_hold_children_map[$node_id];
  201.            
  202.             // Borrar referencias hacia la lista de espera
  203.             unset($on_hold_children[$node_id]);
  204.             unset($on_hold_children_map[$node_id]);
  205.            
  206.             // Agregar nodos hijos para ordenar, sólo si es necesario
  207.             if ($sort && count($node[$children_key]) > 1
  208.                     && !isset($sortable_children[$node_id])) {
  209.                 $sortable_children[$node_id] = &$node[$children_key];
  210.             }
  211.         }
  212.     }
  213.    
  214.     // Obtener los nodos "huérfanos", donde su parent_id != 0 pero de todas
  215.     // maneras no ha sido encontrado
  216.     if (!empty($on_hold_children)) {
  217.         $orphans = $on_hold_children;
  218.     }
  219.    
  220.     // Ordernar los nodos, sólo si es necesario
  221.     if (!empty($sortable_children)) {
  222.         // Obtener el comparador establecido
  223.         $comparator = $options['comparator'];
  224.        
  225.         foreach ($sortable_children as &$unsorted_children) {
  226.             if (is_callable($comparator)) {
  227.                 usort($unsorted_children, $comparator);
  228.             } else {
  229.                 sort($unsorted_children, $comparator);
  230.             }
  231.         }
  232.     }
  233.    
  234.     return $tree;
  235. }

Ahora, un simple ejemplo de como utilizarlo con una base de datos:

Código PHP:
Ver original
  1. // MySQL info.
  2. define('MYSQL_HOSTNAME', 'localhost');
  3. define('MYSQL_USERNAME', 'root');
  4. define('MYSQL_PASSWORD', '123456');
  5. define('MYSQL_DATABASE', 'test');
  6.  
  7. // Crear conexión
  8. $sql_connection = mysqli_connect(
  9.     MYSQL_HOSTNAME,
  10.     MYSQL_USERNAME,
  11.     MYSQL_PASSWORD,
  12.     MYSQL_DATABASE
  13. );
  14.  
  15. if (!$sql_connection) {
  16.     exit(sprintf(
  17.         'Database connection error: (%s) %s',
  18.         mysqli_connect_errno(),
  19.         mysqli_connect_error()
  20.     ));
  21. } else {
  22.     mysqli_query($sql_connection, 'SET NAMES \'utf8\'');
  23. }
  24.  
  25. // Obtener los datos del grupo "0"
  26. $sql_query = '
  27.     SELECT
  28.         links.id,
  29.         links.name,
  30.         links_hierarchy.group_id,
  31.         links_hierarchy.parent_id,
  32.         links_hierarchy.order_no
  33.     FROM links
  34.     INNER JOIN links_hierarchy
  35.         ON links_hierarchy.link_id = links.id
  36.     WHERE links_hierarchy.group_id = 0
  37.         AND links.id > 0
  38. ';
  39.  
  40. // Ejecutar consulta
  41. $sql_query_result = mysqli_query($sql_connection, $sql_query);
  42. $table = array();
  43.  
  44. while ($row = mysqli_fetch_assoc($sql_query_result)) {
  45.     $table[] = $row;
  46. }
  47.  
  48. mysqli_free_result($sql_query_result);
  49. mysqli_close($sql_connection);
  50.  
  51. // Aplicar shuffle() sólo para demostrarles que no importa el orden, el árbol
  52. // siempre se construirá normalmente
  53. shuffle($table);
  54.  
  55. // Función para el ordenamiento por el campo "name"
  56. function compare_nodes($a, $b) {
  57.     $a = $a['value']['name'];
  58.     $b = $b['value']['name'];
  59.    
  60.     return strcasecmp($a, $b);
  61. }
  62.  
  63. // Crear árbol
  64. $tree = table_tree(
  65.     $table,
  66.     true,
  67.     array(
  68.         'comparator' => 'compare_nodes'
  69.     )
  70. );
  71.  
  72. // Obtener el nodo con el id "24"
  73. $node = $tree['nodes'][24];
  74. // Obtener la referencia al nodo padre
  75. $parent_node = $node['parent'];
  76. // Obtener el nodo hijo no. 3, suponiendo que existe
  77. $child_node_3 = $node['children'][2];
  78. // Obtener el valor del nodo (la fila original, obtenida de la consulta SQL)
  79. $node_value = $node['value'];
  80.  
  81. // Los nombres de las claves, serán distintas si las especificamos en el
  82. // parámetro $options, pero estas son utilizadas por defecto

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #3 (permalink)  
Antiguo 27/08/2013, 10:08
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Ahora les muestro la versión orientada a objetos, que básicamente hace lo mismo, sólo que de manera diferente.

En vez de un array, el resultado será una instanca de la clase Diba\GenericTree\TableTree, que extiende la clase Diba\GenericTree\Node, y por ende, implementa a la interface Diba\Tree\Node. Y los hijos son contenidos por una instancia de la clase que implementa a Diba\Tree\NodeList.

Al crear una instancia de Diba\GenericTree\TableTree, opcionalmente se puede pasar una instancia de Diba\GenericTree\TableTreeOptions, por si queremos cambiar los 2 valores de entrada necesarios (clave del id y clave del parent_id) y el comparador para ordenar los nodos.

A continuación, las 2 interfaces:

Diba\Tree\Node
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  * @package Diba
  7.  * @subpackage Tree
  8.  */
  9. namespace Diba\Tree;
  10.  
  11. /**
  12.  * Define los requisitos para un nodo.
  13.  */
  14. interface Node
  15. {
  16.     /**
  17.      * Devuelve una colección de los nodos hijos.
  18.      *
  19.      * @return \Diba\Tree\NodeList
  20.      */
  21.     public function getChildren();
  22.    
  23.     /**
  24.      * Devuelve el nodo padre (si existe), o de lo contrario null.
  25.      *
  26.      * @return \Diba\Tree\Node|null
  27.      */
  28.     public function getParent();
  29.    
  30.     /**
  31.      * Devuelve el valor del nodo.
  32.      *
  33.      * @return mixed
  34.      */
  35.     public function getValue();
  36.    
  37.     /**
  38.      * Indica si el nodo contiene hijos.
  39.      *
  40.      * @return bool
  41.      */
  42.     public function hasChildren();
  43.    
  44.     /**
  45.      * Indica si el nodo contiene padre.
  46.      *
  47.      * @return bool
  48.      */
  49.     public function hasParent();
  50. }

Diba\Tree\NodeList
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  * @package Diba
  7.  * @subpackage Tree
  8.  */
  9. namespace Diba\Tree;
  10.  
  11. use ArrayAccess;
  12. use Countable;
  13. use IteratorAggregate;
  14.  
  15. /**
  16.  * Define los requisitos para una colección de nodos.
  17.  */
  18. interface NodeList extends ArrayAccess, Countable, IteratorAggregate
  19. {
  20.     /**
  21.      * Agrega un nodo a la colección.
  22.      *
  23.      * @param \Diba\Tree\Node $node El nodo que será agregado.
  24.      * @return void
  25.      */
  26.     public function add(Node $node);
  27.    
  28.     /**
  29.      * Limpia la colección de nodos.
  30.      *
  31.      * @return void
  32.      */
  33.     public function clear();
  34.    
  35.     /**
  36.      * Indica si la colección contiene el nodo especificado.
  37.      *
  38.      * @param \Diba\Tree\Node $node El nodo a buscar.
  39.      * @return bool
  40.      */
  41.     public function contains(Node $node);
  42.    
  43.     /**
  44.      * Indica si la colección de nodos está vacía.
  45.      *
  46.      * @return bool
  47.      */
  48.     public function isEmpty();
  49.    
  50.     /**
  51.      * Quita el nodo especificado de la colección.
  52.      *
  53.      * @param \Diba\Tree\Node $node El nodo a quitar.
  54.      * @return void
  55.      */
  56.     public function remove(Node $node);
  57.    
  58.     /**
  59.      * Devuelve una copia de la colección, como array.
  60.      *
  61.      * @return array
  62.      */
  63.     public function toArray();
  64. }

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #4 (permalink)  
Antiguo 27/08/2013, 10:11
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Ahora, las implementaciones para la construcción del árbol.

Diba\GenericTree\Node
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  * @package Diba
  7.  * @subpackage GenericTree
  8.  */
  9. namespace Diba\GenericTree;
  10.  
  11. use Diba\Tree\Node as INode;
  12. use Diba\Tree\NodeList as INodeList;
  13. use InvalidArgumentException;
  14.  
  15. /**
  16.  * Implementación de \Diba\Tree\Node.
  17.  */
  18. class Node implements INode
  19. {
  20.     /**
  21.      * Colección de nodos hijos.
  22.      *
  23.      * @var \Diba\Tree\NodeList
  24.      */
  25.     protected $children;
  26.    
  27.     /**
  28.      * El nodo padre.
  29.      *
  30.      * @var \Diba\Tree\Node
  31.      */
  32.     protected $parent;
  33.    
  34.     /**
  35.      * El valor del nodo.
  36.      *
  37.      * @var mixed
  38.      */
  39.     protected $value;
  40.    
  41.     // \Diba\Tree\Node
  42.    
  43.     /**
  44.      * Devuelve una colección de los nodos hijos.
  45.      *
  46.      * @return \Diba\Tree\NodeList
  47.      */
  48.     public function getChildren()
  49.     {
  50.         if (!$this->children instanceof INodeList) {
  51.             $this->children = new NodeList();
  52.         }
  53.        
  54.         return $this->children;
  55.     }
  56.    
  57.     /**
  58.      * Devuelve el nodo padre (si existe), o de lo contrario null.
  59.      *
  60.      * @return \Diba\Tree\Node|null
  61.      */
  62.     public function getParent()
  63.     {
  64.         return $this->parent;
  65.     }
  66.    
  67.     /**
  68.      * Devuelve el valor del nodo.
  69.      *
  70.      * @return mixed
  71.      */
  72.     public function getValue()
  73.     {
  74.         return $this->value;
  75.     }
  76.    
  77.     /**
  78.      * Indica si el nodo contiene hijos.
  79.      *
  80.      * @return bool
  81.      */
  82.     public function hasChildren()
  83.     {
  84.         $hasChildren = false;
  85.        
  86.         if (isset($this->children)) {
  87.             if (!$this->children->isEmpty()) {
  88.                 $hasChildren = true;
  89.             }
  90.         }
  91.        
  92.         return $hasChildren;
  93.     }
  94.    
  95.     /**
  96.      * Indica si el nodo contiene padre.
  97.      *
  98.      * @return bool
  99.      */
  100.     public function hasParent()
  101.     {
  102.         return isset($this->parent);
  103.     }
  104.    
  105.     // \Diba\GenericTree\Node
  106.    
  107.     /**
  108.      * Constructor
  109.      *
  110.      * @param mixed $value El valor para el nodo.
  111.      * @param \Diba\Tree\Node $parent El padre de este nodo.
  112.      * @param \Diba\Tree\NodeList $children Los hijos de este nodo.
  113.      * @return \Diba\GenericTree\Node
  114.      */
  115.     public function __construct(
  116.         $value = null,
  117.         INode $parent = null,
  118.         INodeList $children = null
  119.     ) {
  120.         $this->value = $value;
  121.         $this->parent = $parent;
  122.         $this->children = $children;
  123.     }
  124.    
  125.     /**
  126.      * Reestablece el nodo a su estado inicial.
  127.      *
  128.      * @return void
  129.      */
  130.     public function reset()
  131.     {
  132.         $this->children = null;
  133.         $this->parent = null;
  134.         $this->value = null;
  135.     }
  136.    
  137.     /**
  138.      * Establece los hijos de este nodo.
  139.      *
  140.      * @return void
  141.      */
  142.     public function setChildren(INodeList $children)
  143.     {
  144.         $this->children = $children;
  145.     }
  146.    
  147.     /**
  148.      * Establece el padre de este nodo.
  149.      *
  150.      * @return void
  151.      * @throws \InvalidArgumentException
  152.      * When trying to set a node as its own parent.
  153.      */
  154.     public function setParent(INode $node)
  155.     {
  156.         if ($node === $this) {
  157.             throw new InvalidArgumentException(
  158.                 'Cannot set node as its own parent'
  159.             );
  160.         }
  161.        
  162.         $this->parent = $node;
  163.     }
  164.    
  165.     /**
  166.      * Establece el valor de este nodo.
  167.      *
  168.      * @return void
  169.      */
  170.     public function setValue($value)
  171.     {
  172.         $this->value = $value;
  173.     }
  174. }

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #5 (permalink)  
Antiguo 27/08/2013, 10:12
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Diba\GenericTree\NodeList
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  * @package Diba
  7.  * @subpackage GenericTree
  8.  */
  9. namespace Diba\GenericTree;
  10.  
  11. use Diba\Tree\Node as INode;
  12. use Diba\Tree\NodeList as INodeList;
  13. use ArrayIterator;
  14. use InvalidArgumentException;
  15. use OutOfBoundsException;
  16.  
  17. /**
  18.  * Implementación de \Diba\Tree\NodeList.
  19.  */
  20. class NodeList implements INodeList
  21. {
  22.     /**
  23.      * El comparador para odernar los nodos.
  24.      *
  25.      * @var callable|int
  26.      */
  27.     protected $comparator;
  28.    
  29.     /**
  30.      * Colección de nodos.
  31.      *
  32.      * @var array
  33.      */
  34.     protected $nodes = [];
  35.    
  36.     // \ArrayAccess
  37.    
  38.     /**
  39.      * Indica si un índice existe.
  40.      *
  41.      * @see http://php.net/arrayaccess.offsetexists
  42.      *
  43.      * @param int $index El índice a ser comprobado.
  44.      * @return bool
  45.      */
  46.     public function offsetExists($index)
  47.     {
  48.         return (int) $index >= 0 && (int) $index < count($this->nodes);
  49.     }
  50.    
  51.     /**
  52.      * Devuelve el nodo contenido por el índice especificado.
  53.      *
  54.      * @see http://php.net/arrayaccess.offsetget
  55.      *
  56.      * @param int $index El índice del nodo a devolver.
  57.      * @return \Diba\Tree\Node
  58.      * @throws \OutOfBoundsException
  59.      * Si el índice está fuera de rango.
  60.      */
  61.     public function offsetGet($index)
  62.     {
  63.         $index = (int) $index;
  64.        
  65.         if ($index < 0 || $index >= count($this->nodes)) {
  66.             throw new OutOfBoundsException('Index is out of range');
  67.         }
  68.        
  69.         return $this->nodes[$index];
  70.     }
  71.    
  72.     /**
  73.      * Inserta un nodo en el índice especificado, y mueve todos los nodos
  74.      * siguientes hacia la derecha, incrementando su índice en: $index + 1.
  75.      *
  76.      * @see http://php.net/arrayaccess.offsetset
  77.      *
  78.      * @param int $index Índice donde se insertará el nodo.
  79.      * @param mixed $node El nodo a insertar.
  80.      * @return void
  81.      * @throws \InvalidArgumentException
  82.      * Si el nodo no implementa a \Diba\Tree\Node.
  83.      * @throws \OutOfBoundsException
  84.      * Si el índice está fuera de rango.
  85.      */
  86.     public function offsetSet($index, $node)
  87.     {
  88.         if (!$node instanceof INode) {
  89.             throw new InvalidArgumentException(
  90.                 'Invalid node type: instance of "\Diba\Tree\Node" expected, '
  91.                     .gettype($node). ' given'
  92.             );
  93.         }
  94.        
  95.         if ($index === null) {
  96.             $this->nodes[] = $node;
  97.            
  98.             return;
  99.         } elseif (!$this->offsetExists($index)) {
  100.             throw new OutOfBoundsException('Index is out of range');
  101.         }
  102.        
  103.         $index = (int) $index;
  104.        
  105.         if ($index === 0) {
  106.             array_unshift($this->nodes, $node);
  107.                
  108.             return;
  109.         }
  110.        
  111.         $right = array_splice($this->nodes, $index);
  112.         $this->nodes[] = $node;
  113.         $this->nodes = array_merge($this->nodes, $right);
  114.     }
  115.    
  116.     /**
  117.      * Quita el nodo contenido por el índice especificado, y mueve todos los
  118.      * nodos siguientes hacia la izquierda, reduciendo su índice en: $index - 1.
  119.      *
  120.      * @see http://php.net/arrayaccess.offsetunset
  121.      *
  122.      * @param int $index Índice del nodo a ser quitado.
  123.      * @return void
  124.      * @throws \OutOfBoundsException
  125.      * Si el índice está fuera de rango.
  126.      */
  127.     public function offsetUnset($index)
  128.     {
  129.         $index = (int) $index;
  130.        
  131.         if ($index < 0 || $index >= count($this->nodes)) {
  132.             throw new OutOfBoundsException('Index is out of range');
  133.         }
  134.        
  135.         array_splice($this->nodes, $index, 1);
  136.     }
  137.    
  138.     // \Countable
  139.    
  140.     /**
  141.      * Devuelve el número de nodos en ésta colección.
  142.      *
  143.      * @see http://php.net/class.countable
  144.      *
  145.      * @return int
  146.      */
  147.     public function count()
  148.     {
  149.         return count($this->nodes);
  150.     }
  151.    
  152.     // \IteratorAggregate
  153.    
  154.     /**
  155.      * Devuelve un iterador externo.
  156.      *
  157.      * @see http://php.net/class.iteratoraggregate
  158.      *
  159.      * @return \Iterator
  160.      */
  161.     public function getIterator()
  162.     {
  163.         return new ArrayIterator($this->nodes);
  164.     }
  165.    
  166.     // \Diba\Tree\NodeList
  167.    
  168.     /**
  169.      * Agrega un nodo a la colección.
  170.      *
  171.      * @param \Diba\Tree\Node $node El nodo que será agregado.
  172.      * @return void
  173.      */
  174.     public function add(INode $node)
  175.     {
  176.         $this->nodes[] = $node;
  177.     }
  178.    
  179.     /**
  180.      * Limpia la colección de nodos.
  181.      *
  182.      * @return void
  183.      */
  184.     public function clear()
  185.     {
  186.         $this->nodes = [];
  187.     }
  188.    
  189.     /**
  190.      * Indica si la colección contiene el nodo especificado.
  191.      *
  192.      * @param \Diba\Tree\Node $node El nodo a buscar.
  193.      * @return bool
  194.      */
  195.     public function contains(INode $node)
  196.     {
  197.         $search = array_search($node, $this->nodes, true);
  198.        
  199.         return ($search !== false);
  200.     }
  201.    
  202.     /**
  203.      * Indica si la colección de nodos está vacía.
  204.      *
  205.      * @return bool
  206.      */
  207.     public function isEmpty()
  208.     {
  209.         return empty($this->nodes);
  210.     }
  211.    
  212.     /**
  213.      * Quita el nodo especificado de la colección.
  214.      *
  215.      * @param \Diba\Tree\Node $node El nodo a quitar.
  216.      * @return void
  217.      */
  218.     public function remove(INode $node)
  219.     {
  220.         $previousCount = count($this->nodes);
  221.        
  222.         while (($index = $this->indexOf($node)) > -1) {
  223.             unset($this->nodes[$index]);
  224.         }
  225.        
  226.         if ($previousCount < count($this->nodes)) {
  227.             $this->nodes = array_values($this->nodes);
  228.         }
  229.     }
  230.    
  231.     /**
  232.      * Devuelve una copia de la colección, como array.
  233.      *
  234.      * @return array
  235.      */
  236.     public function toArray()
  237.     {
  238.         return $this->nodes;
  239.     }
  240.    
  241.     // \Diba\GenericTree\NodeList
  242.    
  243.     /**
  244.      * Inserta un nodo en el índice especificado, y mueve todos los nodos
  245.      * siguientes hacia la derecha, incrementando su índice en: $index + 1.
  246.      *
  247.      * @param int $index Índice del nodo a ser insertado.
  248.      * @param \Diba\Tree\Node $node El nodo a insertar.
  249.      * @return void
  250.      * @throws \OutOfBoundsException
  251.      * Si el índice está fuera de rango.
  252.      */
  253.     public function addAt($index, INode $node)
  254.     {
  255.         $this->offsetSet($index, $node);
  256.     }
  257.    
  258.     /**
  259.      * Devuelve el nodo contenido por el índice especificado.
  260.      *
  261.      * @param int $index El índice del nodo a obtener.
  262.      * @return \Diba\Tree\Node
  263.      * @throws \OutOfBoundsException
  264.      * Si el índice está fuera de rango.
  265.      */
  266.     public function get($index)
  267.     {
  268.         return $this->offsetGet($index);
  269.     }
  270.    
  271.     /**
  272.      * Devuelve el comparador de nodos.
  273.      *
  274.      * @return callable|int
  275.      */
  276.     public function getComparator()
  277.     {
  278.         return $this->comparator;
  279.     }
  280.    
  281.     /**
  282.      * Devuelve el primer índice de un nodo, si existe.
  283.      *
  284.      * @param \Diba\Tree\Node $node El nodo a buscar.
  285.      * @return int -1 si el índice no existe.
  286.      */
  287.     public function indexOf(INode $node)
  288.     {
  289.         $search = array_search($node, $this->nodes, true);
  290.        
  291.         return ($search !== false ? $search : -1);
  292.     }
  293.    
  294.     /**
  295.      * Une la colección de nodos especificada con ésta mísma.
  296.      *
  297.      * @param \Diba\Tree\NodeList $nodeList La colección de nodos a unir.
  298.      * @return bool Verdadero si ésta colección ha cambiado.
  299.      */
  300.     public function merge(INodeList $nodeList)
  301.     {
  302.         $previousCount = count($this->nodes);
  303.        
  304.         // Manipular hasta 3 nodos manualmente
  305.         switch ($nodeList->count()) {
  306.             case 0:
  307.                 return false;
  308.                 break;
  309.             case 1:
  310.                 $this->nodes[] = $nodeList->get(0);
  311.                 break;
  312.             case 2:
  313.                 $this->nodes[] = $nodeList->get(0);
  314.                 $this->nodes[] = $nodeList->get(1);
  315.                 break;
  316.             case 3:
  317.                 $this->nodes[] = $nodeList->get(0);
  318.                 $this->nodes[] = $nodeList->get(1);
  319.                 $this->nodes[] = $nodeList->get(2);
  320.                 break;
  321.             default:
  322.                 if ($previousCount < 1) {
  323.                     $this->nodes = $nodeList->toArray();
  324.                 } else {
  325.                     array_merge($this->nodes, $nodeList->toArray());
  326.                 }
  327.                 break;
  328.         }
  329.        
  330.         return count($this->nodes) > $previousCount;
  331.     }
  332.    
  333.     /**
  334.      * Quita el nodo contenido por el índice especificado, y mueve todos los
  335.      * nodos siguientes hacia la izquierda, reduciendo su índice en: $index - 1.
  336.      *
  337.      * @param int $index Índice del nodo a ser quitado.
  338.      * @return \Diba\Tree\Node El nodo previo en el índice especificado.
  339.      * @throws \OutOfBoundsException
  340.      * Si el índice está fuera de rango.
  341.      */
  342.     public function removeAt($index)
  343.     {
  344.         $previousNode = $this->offsetGet($index);
  345.         $this->offsetUnset($index);
  346.        
  347.         return $previousNode;
  348.     }
  349.    
  350.     /**
  351.      * Establece el nodo en el índice especificado, y sobreescribe al anterior.
  352.      *
  353.      * @param int $index El índice del nodo a establecer.
  354.      * @param \Diba\Tree\Node $node El nodo a establecer.
  355.      * @return \Diba\Tree\Node El nodo previo, si existía.
  356.      * @throws \OutOfBoundsException
  357.      * Si el índice está fuera de rango.
  358.      */
  359.     public function set($index, INode $node)
  360.     {
  361.         $previousNode = $this->offsetGet($index);
  362.         $this->nodes[$index] = $node;
  363.        
  364.         return $previousNode;
  365.     }
  366.    
  367.     /**
  368.      * Establece el nodo padre para todos los nodos de ésta colección.
  369.      *
  370.      * @param \Diba\Tree\Node $node El nodo padre.
  371.      * @return bool Verdadero si la ejecución ha cambiado a los nodos.
  372.      */
  373.     public function setParent(INode $node)
  374.     {
  375.         // Manipular hasta 3 nodos manualmente
  376.         switch (count($this->nodes)) {
  377.             case 0:
  378.                 return false;
  379.                 break;
  380.             case 1:
  381.                 $this->nodes[0]->setParent($node);
  382.                 break;
  383.             case 2:
  384.                 $this->nodes[0]->setParent($node);
  385.                 $this->nodes[1]->setParent($node);
  386.                 break;
  387.             case 3:
  388.                 $this->nodes[0]->setParent($node);
  389.                 $this->nodes[1]->setParent($node);
  390.                 $this->nodes[2]->setParent($node);
  391.                 break;
  392.             default:
  393.                 foreach ($this->nodes as $childNode) {
  394.                     $childNode->setParent($node);
  395.                 }
  396.                 break;
  397.         }
  398.        
  399.         return true;
  400.     }
  401.    
  402.     /**
  403.      * Establece el comparador, y devuelve el anterior.
  404.      *
  405.      * @param callable|int $comparator El callable o integer "flags".
  406.      * @return callable|int
  407.      * @throws \InvalidArgumentException
  408.      * Cuando el argumento $comparator no es de tipo callable o integer.
  409.      */
  410.     public function setComparator($comparator)
  411.     {
  412.         if (is_callable($comparator) || is_int($comparator)) {
  413.             $previousComparator = $this->comparator;
  414.             $this->comparator = $comparator;
  415.            
  416.             return $previousComparator;
  417.         } else {
  418.             throw new InvalidArgumentException(sprintf(
  419.                 'Invalid $comparator type: callable/int expected, %s given',
  420.                 gettype($comparator)
  421.             ));
  422.         }
  423.     }
  424.    
  425.     /**
  426.      * Ordena los nodos de acuerdo al comparador establecido.
  427.      *
  428.      * @return void
  429.      */
  430.     public function sort()
  431.     {
  432.         if (is_callable($this->comparator)) {
  433.             usort($this->nodes, $this->comparator);
  434.         } else {
  435.             sort($this->nodes, $this->comparator);
  436.         }
  437.     }
  438. }

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #6 (permalink)  
Antiguo 27/08/2013, 10:14
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Diba\GenericTree\TableTreeOptions
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  * @package Diba
  7.  * @subpackage GenericTree
  8.  */
  9. namespace Diba\GenericTree;
  10.  
  11. use Diba\Tree\Node as INode;
  12. use Diba\Tree\NodeList as INodeList;
  13. use InvalidArgumentException;
  14.  
  15. /**
  16.  * Opciones utilizdas por una instancia de \Diba\GenericTree\TableTree.
  17.  */
  18. class TableTreeOptions
  19. {
  20.     /**
  21.      * El comparador utilizado para ordenar los nodos, o "flags" utlizados por
  22.      * la función usort().
  23.      *
  24.      * @see \Diba\GenericTree\NodeList::getComparator()
  25.      * @see \Diba\GenericTree\NodeList::sort()
  26.      *
  27.      * @var callable|int
  28.      */
  29.     private $comparator;
  30.    
  31.     /**
  32.      * Nombre de la clave del id de entrada.
  33.      *
  34.      * @var string
  35.      */
  36.     private $idKey;
  37.    
  38.     /**
  39.      * Nombre de la clave del id padre de entrada.
  40.      *
  41.      * @var string
  42.      */
  43.     private $parentIdKey;
  44.    
  45.     /**
  46.      * Constructor
  47.      *
  48.      * @param string $idKey Clave de entrada del id.
  49.      * @param string $parentIdKey Clave de entrada del id padre.
  50.      * @param callable|int $comparator El comparador de nodos.
  51.      * @return \Diba\GenericTree\TableTreeOptions
  52.      * @throws \InvalidArgumentException
  53.      * Cuando los argumentos $idKey y/o $parentIdKey no son de tipo string.
  54.      * @throws \InvalidArgumentException
  55.      * Cuando el argumento $comparator no es de tipo callable o integer.
  56.      */
  57.     public function __construct(
  58.         $idKey = 'id',
  59.         $parentIdKey = 'parentId',
  60.         $comparator = \SORT_REGULAR
  61.     ) {
  62.         $this->setComparator($comparator);
  63.         $this->setIdKey($idKey);
  64.         $this->setParentIdKey($parentIdKey);
  65.     }
  66.    
  67.     /**
  68.      * Devuelve el comparador establecido.
  69.      *
  70.      * @return callable|int
  71.      */
  72.     public function getComparator()
  73.     {
  74.         return $this->comparator;
  75.     }
  76.    
  77.     /**
  78.      * Devuelve el nombre de la clave id establecida.
  79.      *
  80.      * @return string
  81.      */
  82.     public function getIdKey()
  83.     {
  84.         return $this->idKey;
  85.     }
  86.    
  87.     /**
  88.      * Devuelve el nombre de la clave id padre establecida.
  89.      *
  90.      * @return string
  91.      */
  92.     public function getParentIdKey()
  93.     {
  94.         return $this->parentIdKey;
  95.     }
  96.    
  97.     /**
  98.      * Establece el comparador, y devuelve el anterior.
  99.      *
  100.      * @param callable|int $comparator El nuevo comparador.
  101.      * @return callable|int
  102.      * @throws \InvalidArgumentException
  103.      * Cuando el argumento $comparator no es de tipo callable o integer.
  104.      */
  105.     public function setComparator($comparator)
  106.     {
  107.         if (is_callable($comparator) || is_int($comparator)) {
  108.             $previousComparator = $this->comparator;
  109.             $this->comparator = $comparator;
  110.            
  111.             return $previousComparator;
  112.         } else {
  113.             throw new InvalidArgumentException(sprintf(
  114.                 'Invalid $comparator type: callable/int expected, %s given',
  115.                 gettype($comparator)
  116.             ));
  117.         }
  118.     }
  119.    
  120.     /**
  121.      * Establece el nombre de la clave del id, y devuelve el anterior.
  122.      *
  123.      * @param string $name La nueva clave del id.
  124.      * @return string
  125.      * @throws \InvalidArgumentException
  126.      * Cuando el argumento $name no es de tipo string.
  127.      */
  128.     public function setIdKey($name)
  129.     {
  130.         if (!is_string($name)) {
  131.             throw new InvalidArgumentException(sprintf(
  132.                 'Invalid $name type: string expected, %s given',
  133.                 gettype($name)
  134.             ));
  135.         }
  136.        
  137.         $previousIdKey = $this->idKey;
  138.         $this->idKey = $name;
  139.        
  140.         return $previousIdKey;
  141.     }
  142.    
  143.     /**
  144.      * Establece el nombre de la clave del id padre, y devuelve el anterior.
  145.      *
  146.      * @param string $name La nueva clave del id padre.
  147.      * @return string
  148.      * @throws \InvalidArgumentException
  149.      * Cuando el argumento $name no es de tipo string.
  150.      */
  151.     public function setParentIdKey($name)
  152.     {
  153.         if (!is_string($name)) {
  154.             throw new InvalidArgumentException(sprintf(
  155.                 'Invalid $name type: string expected, %s given',
  156.                 gettype($name)
  157.             ));
  158.         }
  159.        
  160.         $previousParentIdKey = $this->parentIdKey;
  161.         $this->parentIdKey = $name;
  162.        
  163.         return $previousParentIdKey;
  164.     }
  165. }

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #7 (permalink)  
Antiguo 27/08/2013, 10:15
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Diba\GenericTree\TableTree
Código PHP:
Ver original
  1. <?php
  2.  
  3. /**
  4.  * @author Diego P. M. Baltar <[email protected]>
  5.  * @copyright (c) 2013
  6.  * @package Diba
  7.  * @subpackage GenericTree
  8.  */
  9. namespace Diba\GenericTree;
  10.  
  11. use Diba\Tree\Node as INode;
  12. use Diba\Tree\NodeList as INodeList;
  13.  
  14. /**
  15.  * Implementación genérica de un árbol, construída desde una tabla.
  16.  */
  17. class TableTree extends Node
  18. {
  19.     /**
  20.      * Mapa de nodos por su id.
  21.      *
  22.      * @var array
  23.      */
  24.     protected $nodes = [];
  25.    
  26.     /**
  27.      * Opciones de entrada y salida.
  28.      *
  29.      * @var \Diba\GenericTree\TableTreeOptions
  30.      */
  31.     protected $options;
  32.    
  33.     /**
  34.      * Mapa de colección de nodos "huérfanos" por su id de padre, es decir,
  35.      * aquellos que deberían tener un padre pero no fue encontrado.
  36.      *
  37.      * @var array
  38.      */
  39.     protected $orphans = [];
  40.    
  41.     /**
  42.      * Constructor
  43.      *
  44.      * @param \Diba\GenericTree\TableTreeOptions $options
  45.      * Opciones de entrada y salida.
  46.      * @return \Diba\GenericTree\TableTree
  47.      */
  48.     public function __construct(TableTreeOptions $options = null)
  49.     {
  50.         if ($options === null) {
  51.             $this->options = new TableTreeOptions();
  52.         } else {
  53.             $this->options = $options;
  54.         }
  55.     }
  56.    
  57.     /**
  58.      * Construye el árbol desde una tabla.
  59.      *
  60.      * @todo Detectar errores recursivos.
  61.      * @todo Detectar multiples ubicaciones (en un mísmo grupo).
  62.      *
  63.      * @param array $table La tabla de datos.
  64.      * @param bool $sort Indica si los nodos deben ser ordenados.
  65.      * @return bool Verdadero si el árbol ha cambiado.
  66.      */
  67.     public function build(array $table, $sort = false)
  68.     {
  69.         if (empty($table)) {
  70.             return false;
  71.         }
  72.        
  73.         // Acceso corto a los nombres de las claves de los ids de entrada
  74.         $idKey = $this->options->getIdKey();
  75.         $parentIdKey = $this->options->getParentIdKey();
  76.        
  77.         // Datos temporales
  78.         $onHold = [];            // The "on hold" list
  79.         $onHoldChildren = [];    // Children on hold nodes
  80.         $onHoldChildrenMap = []; // Children on hold map
  81.         $sortableChildren = [];  // Children that should be sorted
  82.        
  83.         // Agregar el árbol en sí, al mapa de nodos bajo el id "0"
  84.         $this->nodes[0] = $this;
  85.        
  86.         // Construir árbol
  87.         foreach ($table as $index => $row) {
  88.             // Forzar fila hacia un array, para obtener los ids
  89.             $row = (array) $row;
  90.             $nodeId = $row[$idKey];
  91.             $nodeParentId = $row[$parentIdKey];
  92.            
  93.             // Ignorar filas con clave "0"
  94.             if ((int) $nodeId === 0) {
  95.                 continue;
  96.             }
  97.            
  98.             // Crear nodo
  99.             $node = new Node($table[$index]);
  100.            
  101.             // Indica si el nodo padre del nodo actual está en espera
  102.             $parentIsOnHold = false;
  103.            
  104.             // ¿Se ha encontrado ya el nodo padre de este nodo?
  105.             if (isset($this->nodes[$nodeParentId])) {
  106.                 $parent = $this->nodes[$nodeParentId];
  107.             } elseif (isset($onHold[$nodeParentId])) {
  108.                 $onHoldKey = $onHold[$nodeParentId];
  109.                 $parent = $onHoldChildrenMap[$onHoldKey][$nodeParentId];
  110.                 $parentIsOnHold = true;
  111.             } elseif (isset($parent)) {
  112.                 unset($parent);
  113.             }
  114.            
  115.             // Dar nodo a su padre (si éste ha sido encontrado)
  116.             if (isset($parent)) {
  117.                 $parentChildren = $parent->getChildren();
  118.                 // Establecer el nodo padre y agregar sus hijos
  119.                 $node->setParent($parent);
  120.                 $parentChildren->add($node);
  121.                
  122.                 if ($parentIsOnHold) {
  123.                     // Agregar nodo al mapa de lista de espera
  124.                     $onHold[$nodeId] = $onHold[$nodeParentId];
  125.                     $onHoldChildrenMap[$onHold[$nodeId]][$nodeId] = $node;
  126.                 } else {
  127.                     // Agregar nodo al mapa final
  128.                     $this->nodes[$nodeId] = $node;
  129.                 }
  130.                
  131.                 // Agregar nodos hijos para ordenar, sólo si es necesario
  132.                 if ($sort && $parentChildren->count() > 1
  133.                         && !isset($sortableChildren[$nodeParentId])) {
  134.                     $sortableChildren[$nodeParentId] = $parentChildren;
  135.                 }
  136.             } else {
  137.                 // Agregar nodo a la lista de espera
  138.                 $onHold[$nodeId] = $nodeParentId;
  139.                
  140.                 if (!isset($onHoldChildren[$nodeParentId])) {
  141.                     $onHoldChildren[$nodeParentId] = new NodeList();
  142.                 }
  143.                
  144.                 if (!isset($onHoldChildrenMap[$nodeParentId])) {
  145.                     $onHoldChildrenMap[$nodeParentId] = [];
  146.                 }
  147.                
  148.                 // Agregar el nodo al mapa de lista de espera
  149.                 $onHoldChildren[$nodeParentId]->add($node);
  150.                 $onHoldChildrenMap[$nodeParentId][$nodeId] = $node;
  151.             }
  152.            
  153.             // ¿És este nodo el padre de alguno de los nodos en espera?
  154.             if (isset($onHoldChildren[$nodeId])) {
  155.                 $nodeChildren = $node->getChildren();
  156.                
  157.                 // Establecer el nodo padre, y agregar a sus hijos
  158.                 $onHoldChildren[$nodeId]->setParent($node);
  159.                 $nodeChildren->merge($onHoldChildren[$nodeId]);
  160.                
  161.                 // Agregar nodos al mapa final
  162.                 $this->nodes+= $onHoldChildrenMap[$nodeId];
  163.                
  164.                 // Borrar referencias del nodo hacia la lista de espera
  165.                 unset($onHoldChildren[$nodeId]);
  166.                 unset($onHoldChildrenMap[$nodeId]);
  167.                
  168.                 // Agregar nodos hijos para ordenar, sólo si es necesario
  169.                 if ($sort && $nodeChildren->count() > 1
  170.                         && !isset($sortableChildren[$nodeId])) {
  171.                     $sortableChildren[$nodeId] = $nodeChildren;
  172.                 }
  173.             }
  174.         }
  175.        
  176.         // Obtener los nodos "huérfanos", donde su parent_id != 0 pero de todas
  177.         // maneras no ha sido encontrado
  178.         if (!empty($onHoldChildren)) {
  179.             $this->orphans = $onHoldChildren;
  180.         }
  181.        
  182.         // Ordenar los nodos, sólo si es necesario
  183.         if (!empty($sortableChildren)) {
  184.             foreach ($sortableChildren as $unsortedChildren) {
  185.                 $unsortedChildren->setComparator(
  186.                     $this->options->getComparator()
  187.                 );
  188.                 $unsortedChildren->sort();
  189.             }
  190.         }
  191.        
  192.         return true;
  193.     }
  194.    
  195.     /**
  196.      * Devuelve un nodo obtenido por su id.
  197.      *
  198.      * @param scalar $id El id del nodo a buscar.
  199.      * @return \Diba\Tree\Node|null
  200.      */
  201.     public function getNodeById($id)
  202.     {
  203.         $id = (string) $id;
  204.         $node = null;
  205.        
  206.         if (isset($this->nodes[$id])) {
  207.             $node = $this->nodes[$id];
  208.         }
  209.        
  210.         return $node;
  211.     }
  212.    
  213.     /**
  214.      * Devuelve la instancia de las opciones de entrada y salida.
  215.      *
  216.      * @return \Diba\GenericTree\TableTreeOptions
  217.      */
  218.     public function getOptions()
  219.     {
  220.         return $this->options;
  221.     }
  222.    
  223.     /**
  224.      * Devuelve una colección de nodos "huérfanos", agrupados por el id de su
  225.      * padre (no encontrado).
  226.      *
  227.      * @return array
  228.      */
  229.     public function getOrphans()
  230.     {
  231.         return $this->orphans;
  232.     }
  233.    
  234.     /**
  235.      * Reestablece el árbol.
  236.      *
  237.      * @return void
  238.      * @override \Diba\GenericTree\Node::reset()
  239.      */
  240.     public function reset()
  241.     {
  242.         $this->nodes = [];
  243.         $this->orphans = [];
  244.        
  245.         parent::reset();
  246.     }
  247.    
  248.     /**
  249.      * Establece las opciones de entrada y salida, y devuelve las anteriores.
  250.      *
  251.      * @param \Diba\GenericTree\TableTreeOptions $options
  252.      * La nueva instancia de las opciones de entrada y salida.
  253.      * @return \Diba\GenericTree\TableTreeOptions
  254.      */
  255.     public function setOptions(TableTreeOptions $options)
  256.     {
  257.         $previousOptions = $this->options;
  258.         $this->options = $options;
  259.        
  260.         return $previousOptions;
  261.     }
  262. }

(Continúa en comentarios)
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #8 (permalink)  
Antiguo 27/08/2013, 10:16
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Ahora, un ejemplo igual al anterior, pero con el código orientado a objetos:

Nota: sólo por si acaso, les comento que me gusta utilizar siempre "camelCase" en código orientado a objetos y clases, y con guión bajo en funciones. Por eso mísmo, notar ésta diferencia: en las opciones por defecto de la función, utilizan guión bajo, y en la orientada a objetos, "camelCase" (por eso el alias en la consulta).

Código PHP:
Ver original
  1. <?php
  2.  
  3. use Diba\GenericTree\Node;
  4. use Diba\GenericTree\NodeList;
  5. use Diba\GenericTree\TableTree;
  6. use Diba\GenericTree\TableTreeOptions;
  7. use Diba\GenericTree\TableTreeHelper;
  8.  
  9. // MySQL info.
  10. define('MYSQL_HOSTNAME', 'localhost');
  11. define('MYSQL_USERNAME', 'root');
  12. define('MYSQL_PASSWORD', '123456');
  13. define('MYSQL_DATABASE', 'test');
  14.  
  15. // Crear conexión
  16. try {
  17.     $sqlConnection = new PDO(
  18.         'mysql:dbname=' .MYSQL_DATABASE. ';host=' .MYSQL_HOSTNAME,
  19.         MYSQL_USERNAME,
  20.         MYSQL_PASSWORD
  21.     );
  22. } catch (PDOException $e) {
  23.     exit(sprintf(
  24.         'Database connection error: %s', $e->getMessage()
  25.     ));
  26. }
  27.  
  28. $sqlConnection->query('SET NAMES \'utf8\'');
  29.  
  30. // Obtener todas las filas del grupo "0"
  31. $sqlQuery = '
  32.     SELECT
  33.         links.id,
  34.         links.name,
  35.         links_hierarchy.group_id AS groupId,
  36.         links_hierarchy.parent_id AS parentId,
  37.         links_hierarchy.order_no AS orderNo
  38.     FROM links
  39.     INNER JOIN links_hierarchy
  40.         ON links_hierarchy.link_id = links.id
  41.     WHERE links_hierarchy.group_id = :group
  42.         AND links.id > 0
  43. ';
  44.  
  45. // Crear "statement"
  46. $sqlStatement = $sqlConnection->prepare($sqlQuery);
  47. $sqlStatement->bindValue(':group', 0, PDO::PARAM_INT);
  48. $sqlStatement->execute();
  49.  
  50. // Obtener los resultados
  51. $sqlResult = $sqlStatement->fetchAll(PDO::FETCH_OBJ);
  52.  
  53. // Aplicar shuffle() sólo para demostrarles que no importa el orden, el árbol
  54. // siempre se construirá normalmente
  55. shuffle($sqlResult);
  56.  
  57. // Crear árbol desde la tabla de datos
  58. $treeOptions = new TableTreeOptions();
  59. $treeOptions->setIdKey('id');
  60. $treeOptions->setParentIdKey('parentId');
  61. $treeOptions->setComparator(function($a, $b) {
  62.     $a = $a->getValue()->name;
  63.     $b = $b->getValue()->name;
  64.    
  65.     return strcasecmp($a, $b);
  66. });
  67.  
  68. $tree = new TableTree($treeOptions);
  69. $tree->build($sqlResult, true);


Bueno, eso es todo amigos... Espero que les sea útil y les haya gustado.

Les dejo un ejemplo online donde implementé el código con un menú (jQuery Sooperfish Menu Plugin), un breadcrumb, y algo similar a un "Tree View" (jQuery Nested Sortable Plugin) para administrar las ubicaciones con un simple "Drag and Drop":

http://test.dpmbaltar.com.ar/tree/ex...procedural.php

¡Hasta luego!
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)

Última edición por Nisrokh; 27/08/2013 a las 10:33
  #9 (permalink)  
Antiguo 27/08/2013, 10:37
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Les indico un error que recién me doy cuenta... En la clase Diba\GenericTree\TableTreeOptions, las variables de la clase deberían tener los siguientes valores por defecto:

Código PHP:
Ver original
  1. // ...
  2.     private $comparator = \SORT_REGULAR;
  3.     private $idKey = 'id';
  4.     private $parentIdKey = 'parentId';
  5.     // ...

Y ejemplos que me faltaron en la versión orientada a objetos:

Código PHP:
Ver original
  1. // Obtener el nodo con el id "24"
  2. $node = $tree->getNodeById(24);
  3. // Obtener el nodo padre
  4. $parentNode = $node->getParent();
  5. // Obtener el nodo padre, del nodo padre
  6. $parentNodeParent = $node->getParent()->getParent();
  7. // Obtener el nodo hijo no. 3, suponiendo que existe
  8. $childNode3 = $node->getChildren()->get(2);
  9. // Obtener el valor del nodo (la fila original, obtenida de la consulta SQL)
  10. $nodeValue = $node->getValue();

Mis disculpas y saludos.
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)

Última edición por Nisrokh; 27/08/2013 a las 10:43
  #10 (permalink)  
Antiguo 27/08/2013, 12:11
Avatar de carlos_belisario
Colaborador
 
Fecha de Ingreso: abril-2010
Ubicación: Venezuela Maracay Aragua
Mensajes: 3.156
Antigüedad: 14 años
Puntos: 461
Respuesta: [APORTE] Estructura de árbol desde de base de datos

creo que sería un poco más visible ver el aporte en un repo como github de manera que pueda jugarse con los directorios, ver autoload, etc, etc, etc y es mas simple de contribuir y hacerle clone si esta en el tiempo de algún dev de aqui (no lo mire lo suficiente como para darte una opinión por eso no me vez diciendote ningún disparáte )
saludos
__________________
aprende d tus errores e incrementa tu conocimientos
it's not a bug, it's an undocumented feature By @David
php the right way
  #11 (permalink)  
Antiguo 27/08/2013, 12:34
 
Fecha de Ingreso: septiembre-2009
Ubicación: Neuquén
Mensajes: 142
Antigüedad: 14 años, 6 meses
Puntos: 12
De acuerdo Respuesta: [APORTE] Estructura de árbol desde de base de datos

Gracias por tu comentario, crearé el repositorio en Git.
__________________
Amigos de Foros del Web: seamos más solidarios. ¡No dejemos que un tema se valla al final de las páginas con 0 (cero) respuestas! ¡Gracias por su ayuda! :-)
  #12 (permalink)  
Antiguo 27/08/2013, 12:50
Avatar de dashtrash
Colaborador
 
Fecha de Ingreso: abril-2007
Ubicación: Ni en Sevilla,ni en Sanlúcar..qué más da..
Mensajes: 927
Antigüedad: 17 años
Puntos: 270
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Aparte de opiniones estilisticas (si una clase tiene exactamente el mismo nombre que un interface, es que no existe ese interface), cargar *todos* los elementos de un árbol, reconstruirlo en memoria, para devolver un nodo, o la lista de nodos hijos directos de un nodo...
Obtener 1 nodo , o sus hijos directos, se consigue con 1 query, sin necesidad de una clase que reconstruya el árbol entero en memoria.
Incluso para alguna operación más, con arrays se puede crear el código equivalente en unas 4 o 5 líneas.(pista: si el select ya va ordenado por nodo-padre, campo-por-el-que-ordenar-hijos), no es necesario sort, usort,etc).

Por otro lado,si te ha funcionado es porque la tabla que utilizas, tiene sólo unos cuantos elementos.Por eso puedes hacer un select + inner join de toda la tabla.
Supón que la tabla tiene 1 millón de filas.

De todo lo que he visto sobre árboles, especialmente en base de datos, (y hay algún libro exclusivamente dedicado a cómo implementar árboles en SQL) lo que mejor me ha funcionado es mantener el path completo desde el root, hasta el nodo, en un campo de la tabla.
  #13 (permalink)  
Antiguo 27/08/2013, 14:23
 
Fecha de Ingreso: julio-2013
Ubicación: México
Mensajes: 361
Antigüedad: 10 años, 8 meses
Puntos: 55
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Hola, me gusta tu aporte, felicidades.

Le veo un buen potencial de uso genral y reuso.

En lo personal me gusta el uso del "Adjacency List Model" para variables de aplicaciones creadas por el usuario.
Tienes un buen modelo para hacer comparaciones de uso vs "The Nested Set Model".

Es importante que atiendas el concepto completo desde el nodo, no desde el grupo, asi evitaras un uso inecesario de memoria y con ello podras incrementar la flexibilidad y funcionalidad.

Saludos.
  #14 (permalink)  
Antiguo 11/02/2015, 14:44
Avatar de EderBarriosCamargo  
Fecha de Ingreso: marzo-2013
Mensajes: 55
Antigüedad: 11 años, 1 mes
Puntos: 0
Respuesta: [APORTE] Estructura de árbol desde de base de datos

Me gustaría saber el repositorio en Git Para Probar algunas cosas, lo veo bien, solo puedo decir eso.

Gracias por el aprote.

Etiquetas: jerarquia, mysql, tree, treeview
Atención: Estás leyendo un tema que no tiene actividad desde hace más de 6 MESES, te recomendamos abrir un Nuevo tema en lugar de responder al actual.
Respuesta

SíEste tema le ha gustado a 1 personas




La zona horaria es GMT -6. Ahora son las 06:22.