Es muy común que se desea almacenar datos jerárquicos en una tabla de base de datos. Ejemplos de estos datos pueden ser: categorías con subcategorías ilimitadas, los datos relativos a un sistema de menús multinivel o una representación literal de la jerarquía tal como se utiliza para almacenar objetos de control de acceso con la lógica ACL.
Para pequeños árboles de datos, o cuando los datos son de sólo unos pocos niveles de profundidad es simple añadir un campo parent_id a su tabla de base de datos y utilizar este para hacer un seguimiento de cual item es padre de quien. Sin embargo, Cake Viene equipado con este paquete, pero con un comportamiento de gran alcance que le permite utilizar los beneficios de la lógica MPTT sin preocuparse de cualquiera de los entresijos de la técnica - a menos que ud lo desee ;).
Para poder usar la funcionalidad de árbol, la tabla de la base de datos debe tener los 3 campos listados a continuación (enteros todos):
padre - el nombre por defecto del campo es parent_id, para almacenar el id del objeto padre
izquierda - el nombre por defecto del campo es lft, para almacenar el valor lft de la fila actual.
derecha - el nombre por defecto del campo es rght, para almacenar el valor rght de la fila actual.
Si esta familiarizado con la lógina MPTT ud puede preguntarse por que el campos parent existe - simplemente es más facil de realizar ciertas tareas si existe un enlace directo al padre almacenado en la base de datos - tales como la búsquera de los hijos directos.
El comportamiento de arbol tiene muchas cosas incluidas, pero empecemos con un simple ejemplo - crearemos una pequeña tabla en una base de datos y le agregaremos algunos datos:
CREATE TABLE categories (
id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
parent_id INTEGER(10) DEFAULT NULL,
lft INTEGER(10) DEFAULT NULL,
rght INTEGER(10) DEFAULT NULL,
name VARCHAR(255) DEFAULT '',
PRIMARY KEY (id)
);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(1, 'Mis Categorias', NULL, 1, 30);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(2, 'Diversion', 1, 2, 15);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(3, 'Deportes', 2, 3, 8);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(4, 'Surfing', 3, 4, 5);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(5, 'Alpinismo', 3, 6, 7);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(6, 'Amigos', 2, 9, 14);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(7, 'Gerald', 6, 10, 11);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(8, 'Gwendolyn', 6, 12, 13);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(9, 'Trabajo', 1, 16, 29);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(10, 'Reportes', 9, 17, 22);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(11, 'Anual', 10, 18, 19);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(12, 'Status', 10, 20, 21);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(13, 'Viajes', 9, 23, 28);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(14, 'Nacional', 13, 24, 25);
INSERT INTO `categories` (`id`, `name`, `parent_id`, `lft`, `rght`) VALUES(15, 'Internacional', 13, 26, 27);
Para el propósito de verificar que todo está configurado correctamente, podemos crear un metodo de testeo y obtener los contenidos de nuestro arbol de categorias para ver como queda. Con un simple controlador:
<?php
class CategoriesController extends AppController {
var $name = 'Categories';
function index() {
$this->data = $this->Category->generatetreelist(null, null, null, ' ');
debug ($this->data); die;
}
}
?>
y un metodo aun mas simple en el modelo:
<?php
// app/models/category.php
class Category extends AppModel {
var $name = 'Category';
var $actsAs = array('Tree');
}
?>
Podemos verificar como se ve que nuestro arbol de categoria visitando /categories Deberias ver algo como:
Mis Categorias
Diversion
Deportes
Surfing
Alpinismo
Amigos
Gerald
Gwendolyn
Trabajo
Reportes
Anual
Status
Viajes
Nacional
Internacional
En la seccion anterior, usamos datos pre-existentes y chequeamos que se
vieran en forma jerarquica con el método generatetreelist
. Sin
embargo, usualmente agregaríamos los datos de la misma forma que lo
hariamos con cualquier modelo. Por ejemplo:
// pseudo código del controlador
$data['Category']['parent_id'] = 3;
$data['Category']['name'] = 'Skating';
$this->Category->save($data);
Cuando se usa el comportamiento de arbol no es necesario hacer nada mas que configurar el parent_id, y el comportamiento de arbol se encargara del resto. Si no se setea el parent_id el comportamiento de arbol lo agregara al arbol como una entrada en el nivel superior:
// pseudo codigo controlador
$data = array();
$data['Category']['name'] = 'Otra Categoria';
$this->Category->save($data);
Ejecutando estos dos trozos de código alterará el árbol como sigue:
Mis Categorias
Diversion
Deportes
Surfing
Alpinismo
Skating New
Amigos
Gerald
Gwendolyn
Trabajo
Reportes
Anual
Status
Viajes
Nacional
Internacional
Otra Categoria New
Modificar datos es tan transparente como agregar nuevos datos. Si modificas algo, pero no modificas el campo parent_id - la estructura de tus datos permanecera inalterada. Por ejemplo:
// pseudo codigo de controlador
$this->Category->id = 5; // id de Apinismo
$this->Category->save(array('name' =>'Pesca'));
El codigo anterior no modifica el parent_id - incluso si el parent_id es incluido en los datos que son pasados al método save, si el valor no ha sido cambiado, tampoco lo hace la estructura de datos. Entonces, el arbol de datos queda:
Mis Categorias
Diversion
Deportes
Surfing
Pesca Updated
Skating
Amigos
Gerald
Gwendolyn
Trabajo
Reportes
Anual
Status
Viajes
Nacional
Internacional
Otra Categoria
Mover un dato a traves del arbol tambien es simple. Digamos que Pesca no pertenece a Deportes, sino que deberia estar en Otra Categoria. Con el siguiente código:
// pseudo codigo de controlador
$this->Category->id = 5; // id of Pesca
$newParentId = $this->Category->field('id', array('name' => 'Otra Categoria'));
$this->Category->save(array('parent_id' => $newParentId));
Como es de esperar la estructura queda modificada a:
Mis Categorias
Diversion
Deportes
Surfing
Skating
Amigos
Gerald
Gwendolyn
Trabajo
Reportes
Anual
Status
Viajes
Nacional
Internacional
Otra Categoria
Pesca Movido
El comportamiento de arbol provee algunas formas para manejar la eliminación de datos. Para comenzar con un ejemplo simple, diremos que la categoria reportes ya no es necesaria. Para eliminarla y cualquier hijo que ella tenga basta llamar a delete tal como lo harias en cualquier modelo. Por ejemplo, en el siguiente código:
// pseudo codigo de controlador
$this->Category->id = 10;
$this->Category->delete();
El arbol de categorias sería modificado como sigue:
Mis Categorias
Diversion
Deportes
Surfing
Skating
Amigos
Gerald
Gwendolyn
Trabajo
Viajes
Nacional
Internacional
Otras Categorias
Pesca
Usar y manipular datos jerarquicos puede ser algo complejo. Ademas de los metodos de busqueda del nucleo como find(), con los arboles tenemos unos cuantos metodos más para su manipulacion.
Muchos metodos del comportamiento de arbol devuelven y se apoyan en el
orden del campo lft
. Si llamas a find()
y no ordenas por el
campo lft
, o llamas algun metodo de arboles entregandole un tipo de
ordenamiento, quizas obtengas resultados no deseados.
El método children
toma la llave primaria (id) de una fila y retorna
los hijos, por defecto en el roden en que aparecen en el árbol. El
segundo parámetro es opcional y define si se entregan o no sólo los
hijos directos. Usando el ejemplo de la sección anterior:
$allChildren = $this->Category->children(1); // un arreglo plano con 11 valores
// -- o bien --
$this->Category->id = 1;
$allChildren = $this->Category->children(); // un arreglo plano con 11 valores
// Retornar solo los hijos directos
$directChildren = $this->Category->children(1, true); //un arreglo plano con 2 valores
Si quieres un arreglo recursivo utiliza find('threaded')
Tal como el método children
, childCount
toma la llave primaria
(id) y retorna cuantos hijos tiene. El segundo parámetro es opcional y
define si se contarán o no los hijos directos. Usando los datos del
ejemplo anterior:
$totalChildren = $this->Category->childCount(1); // entrega 11
// -- o bien --
$this->Category->id = 1;
$directChildren = $this->Category->childCount(); // entrega 11
//Solo los hijos directos
$numChildren = $this->Category->childCount(1, true); // entrega 2
generatetreelist ($conditions=null, $keyPath=null, $valuePath=null, $spacer= '_', $recursive=null)
This method will return data similar to
`find('list')
</es/view/1022/find-list>`_, with an indented prefix
to show the structure of your data. Below is an example of what you can
expect this method to return.
$conditions
- Uses the same conditional options as find().
$keyPath
- Path to the field to use for the key.
$valuePath
- Path to the field to use for the label.
$spacer
- The string to use in front of each item to indicate
depth.
$recursive
- The number of levels deep to fetch associated
records
All the parameters are optional, with the following defaults:
$conditions
= null
$keyPath
= Model’s primary key
$valuePath
= Model’s displayField
$spacer
= '_'
$recursive
= Model’s recursive setting
$treelist = $this->Category->generatetreelist();
Output:
array(
[1] => "My Categories",
[2] => "_Fun",
[3] => "__Sport",
[4] => "___Surfing",
[16] => "___Skating",
[6] => "__Friends",
[7] => "___Gerald",
[8] => "___Gwendolyn",
[9] => "_Work",
[13] => "__Trips",
[14] => "___National",
[15] => "___International",
[17] => "Other People's Categories",
[5] => "_Extreme fishing"
)
Esta conveniente función, como su nombre lo indica, retorna el nodo padre de cualquier nodo, o false si el nodo no tiene padre (es el nodo raíz). Por ejemplo:
$parent = $this->Category->getparentnode(2); //<- id de Fun
// $parent contiene My categories
getpath( $id = null, $fields = null, $recursive = null )
The “path” when refering to hierachial data is how you get from where you are to the top. So for example the path from the category «International» is:
My Categories
…
Work
Trips
…
International
Using the id of «International» getpath will return each of the parents in turn (starting from the top).
$parents = $this->Category->getpath(15);
// contents of $parents
array(
[0] => array('Category' => array('id' => 1, 'name' => 'My Categories', ..)),
[1] => array('Category' => array('id' => 9, 'name' => 'Work', ..)),
[2] => array('Category' => array('id' => 13, 'name' => 'Trips', ..)),
[3] => array('Category' => array('id' => 15, 'name' => 'International', ..)),
)
Este comportamiento de modelo no sólo trabaja en segundo plano, hay una gran variedad de métodos específicos definidos en este comportamiento para antender todas tus necesidades de datos jerárquicos, y cualquier problema inesperado que pueda surgir en el proceso.
Utilizado para mover un único nodo hacia abajo del árbol. Debes indicar el ID del elemento a mover y un número entero positivo de cuantas posiciones el nodo debería ser movido hacia abajo. Todos los nodos hijos del nodo a mover también serán movidos
Un ejemplo de una acción de controlador (en un controlador llamado Categories) que mueve un nodo hacia abajo del arbol:
function movedown($name = null, $delta = null) {
$cat = $this->Category->findByName($name);
if (empty($cat)) {
$this->Session->setFlash('No hay una categoría de nombre ' . $name);
$this->redirect(array('action' => 'index'), null, true);
}
$this->Category->id = $cat['Category']['id'];
if ($delta > 0) {
$this->Category->moveDown($this->Category->id, abs($delta));
} else {
$this->Session->setFlash('Por favor indique el número de posiciones que el nodo debe ser movido hacia abajo.');
}
$this->redirect(array('action' => 'index'), null, true);
}
Por ejemplo, si quisieras mover la categoría «Sport» un nivel hacia abajo, deberías llamar a: /categories/movedown/Sport/1.
Used to move a single node up the tree. You need to provide the ID of the element to be moved and a positive number of how many positions the node should be moved up. All child nodes will also be moved.
If the node is the first child, or is a top level node with no previous node this method will return false.
Here’s an example of a controller action (in a controller named Categories) that moves a node up the tree:
function moveup($name = null, $delta = null){
$cat = $this->Category->findByName($name);
if (empty($cat)) {
$this->Session->setFlash('There is no category named ' . $name);
$this->redirect(array('action' => 'index'), null, true);
}
$this->Category->id = $cat['Category']['id'];
if ($delta > 0) {
$this->Category->moveUp($this->Category->id, abs($delta));
} else {
$this->Session->setFlash('Please provide a number of positions the category should be moved up.');
}
$this->redirect(array('action' => 'index'), null, true);
}
For example, if you would like to move the category «Gwendolyn» up one position you would request /categories/moveup/Gwendolyn/1. Now the order of Friends will be Gwendolyn, Gerald.
removeFromTree($id=null, $delete=false)
Using this method wil either delete or move a node but retain its
sub-tree, which will be reparented one level higher. It offers more
control than `delete()
</es/view/1316/delete>`_, which for a model
using the tree behavior will remove the specified node and all of its
children.
Taking the following tree as a starting point:
My Categories
Fun
Sport
Surfing
Extreme knitting
Skating
Running the following code with the id for “Sport”
$this->Node->removeFromTree($id);
The Sport node will be become a top level node:
My Categories
Fun
Surfing
Extreme knitting
Skating
Sport Moved
This demonstrates the default behavior of removeFromTree
of moving
the node to have no parent, and re-parenting all children.
If however the following code snippet was used with the id for “Sport”
$this->Node->removeFromTree($id,true);
The tree would become
My Categories
Fun
Surfing
Extreme knitting
Skating
This demonstrates the alternate use for removeFromTree
, the children
have been reparented and “Sport” has been deleted.
reorder ( array('id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true) )
Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters. This method does not change the parent of any node.
$model->reorder(array(
'id' => , //id of record to use as top node for reordering, default: $Model->id
'field' => , //which field to use in reordering, default: $Model->displayField
'order' => , //direction to order, default: 'ASC'
'verify' => //whether or not to verify the tree before reorder, default: true
));
If you have saved your data or made other operations on the model, you
might want to set $model->id = null
before calling reorder
.
Otherwise only the current node and it’s children will be reordered.
Debido a la naturaleza de las estructuras complejas autorreferentes como los árboles y las listas enlazadas, ocasionalmente pueden romperse debido a una llamada poco cuidadosa. Tómalo con calma, ¡no todo está perdido! Tree Behavior contiene varias características que antes no estaban documentadas, diseñadas para recuperarse de tales situaciones.
recover(&$model, $mode = 'parent', $missingParentAction = null)
The mode
parameter is used to specify the source of info that is
valid/correct. The opposite source of data will be populated based upon
that source of info. E.g. if the MPTT fields are corrupt or empty, with
the $mode 'parent'
the values of the parent_id
field will be
used to populate the left and right fields. The missingParentAction
parameter only applies to «parent» mode and determines what to do if the
parent field contains an id that is not present.
Available $mode
options:
'parent'
- use the existing parent_id
’s to update the lft
and rght
fields
'tree'
- use the existing lft
and rght
fields to update
parent_id
Available missingParentActions
options when using mode='parent'
:
null
- do nothing and carry on
'return'
- do nothing and return
'delete'
- delete the node
int
- set the parent_id to this id
// Rebuild all the left and right fields based on the parent_id
$this->Category->recover();
// or
$this->Category->recover('parent');
// Rebuild all the parent_id's based on the lft and rght fields
$this->Category->recover('tree');
reorder(&$model, $options = array())
Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters. This method does not change the parent of any node.
Reordering affects all nodes in the tree by default, however the following options can affect the process:
'id'
- only reorder nodes below this node.
'field
” - field to use for sorting, default is the
displayField
for the model.
'order'
- 'ASC'
for ascending, 'DESC'
for descending
sort.
'verify'
- whether or not to verify the tree prior to resorting.
$options
is used to pass all extra parameters, and has the following
possible keys by default, all of which are optional:
array(
'id' => null,
'field' => $model->displayField,
'order' => 'ASC',
'verify' => true
)
verify(&$model)
Retorna true
si el árbol es valido de otro modo retorna un arreglo
de errores, con campos para el tipo de error, indice incorrecto y el
mensaje.
Cada registro en al arreglo de salida es una arreglo de la forma (tipo, id, mensaje)
type
es bien 'index'
o 'node'
'id'
es el id del nodo erróneo.
'message'
depende del error
$this->Categories->verify();
Salida del ejemplo:
Array
(
[0] => Array
(
[0] => node
[1] => 3
[2] => Valores left y right identicos
)
[1] => Array
(
[0] => node
[1] => 2
[2] => El nodo padre 999 no existe
)
[10] => Array
(
[0] => index
[1] => 123
[2] => Desaparecido
)
[99] => Array
(
[0] => node
[1] => 163
[2] => left mayor que right
)
)