Este es un paquete Laravel 4-10 para trabajar con árboles en bases de datos relacionales.
Laravel 11.0 es compatible desde v6.0.4
Laravel 10.0 es compatible desde v6.0.2
Laravel 9.0 es compatible desde v6.0.1
Laravel 8.0 es compatible desde v6.0.0
Laravel 5.7, 5.8, 6.0, 7.0 es compatible desde v5
Laravel 5.5, 5.6 es compatible desde v4.3
Laravel 5.2, 5.3, 5.4 es compatible desde v4
Laravel 5.1 es compatible con v3
Laravel 4 es compatible con la v2
Contenido:
Teoría
Documentación
Insertar nodos
Recuperando nodos
Eliminar nodos
Comprobación y corrección de coherencia
Alcance
Requisitos
Instalación
Los conjuntos anidados o modelo de conjunto anidado son una forma de almacenar eficazmente datos jerárquicos en una tabla relacional. De wikipedia:
El modelo de conjunto anidado consiste en numerar los nodos según un recorrido de árbol, que visita cada nodo dos veces, asignando números en el orden de visita y en ambas visitas. Esto deja dos números para cada nodo, que se almacenan como dos atributos. Las consultas se vuelven económicas: la pertenencia a la jerarquía se puede probar comparando estos números. La actualización requiere una nueva numeración y, por tanto, es costosa.
NSM muestra un buen rendimiento cuando el árbol se actualiza raramente. Está configurado para ser rápido para obtener nodos relacionados. Es ideal para crear menús o categorías de múltiples profundidades para la tienda.
Supongamos que tenemos un modelo Category
; una variable $node
es una instancia de ese modelo y del nodo que estamos manipulando. Puede ser un modelo nuevo o uno de la base de datos.
Node tiene las siguientes relaciones que son completamente funcionales y se pueden cargar con entusiasmo:
El nodo pertenece al parent
El nodo tiene muchos children
El nodo tiene muchos ancestors
El nodo tiene muchos descendants
Mover e insertar nodos incluye varias consultas a la base de datos, por lo que se recomienda encarecidamente utilizar transacciones.
¡IMPORTANTE! A partir de v4.2.0, la transacción no se inicia automáticamente
Otra nota importante es que las manipulaciones estructurales se difieren hasta que presione save
en el modelo (algunos métodos llaman implícitamente a save
y devuelven el resultado booleano de la operación).
Si el modelo se guarda correctamente, no significa que se haya movido el nodo. Si su aplicación depende de si el nodo realmente ha cambiado su posición, use el método hasMoved
:
if ($nodo->guardar()) {$movido = $nodo->hasMoved(); }
Cuando simplemente crea un nodo, se agregará al final del árbol:
Categoría::crear($atributos); // Guardado como root
$nodo = nueva categoría($atributos);$nodo->save(); // Guardado como root
En este caso, el nodo se considera raíz , lo que significa que no tiene padre.
// #1 Guardar implícito$nodo->saveAsRoot();// #2 Guardar explícito$nodo->makeRoot()->save();
El nodo se agregará al final del árbol.
Si desea que el nodo sea hijo de otro nodo, puede convertirlo en el último o el primer hijo.
En los siguientes ejemplos, $parent
es algún nodo existente.
Hay algunas formas de agregar un nodo:
// #1 Usando inserción diferida$node->appendToNode($parent)->save();// #2 Usando el nodo padre$parent->appendNode($node);// #3 Usando la relación entre padres e hijos$parent- >children()->create($attributes);// #5 Usando la relación padre del nodo$node->parent()->associate($parent)->save();// #6 Usando el padre atributo$nodo->parent_id = $padre->id;$nodo->save();// #7 Usando el método estáticoCategory::create($attributes, $parent);
Y sólo un par de formas de anteponer:
// #1$nodo->prependToNode($padre)->save();// #2$padre->prependNode($nodo);
Puede hacer que $node
sea vecino del nodo $neighbor
utilizando los siguientes métodos:
$neighbor
debe existir, el nodo de destino puede ser nuevo. Si el nodo de destino existe, se moverá a la nueva posición y se cambiará el padre si es necesario.
# Guardar explícito$nodo->despuésNodo($vecino)->guardar();$nodo->antesNodo($vecino)->guardar();# Guardar implícito$nodo->insertarDespuésNodo($vecino);$nodo-> insertBeforeNode($vecino);
Cuando se utiliza el método estático create
en el nodo, se comprueba si los atributos contienen claves children
. Si es así, crea más nodos de forma recursiva.
$nodo = Categoría::create(['nombre' => 'Foo','niños' => [ ['nombre' => 'Bar','niños' => [ [ 'nombre' => 'Baz' ], ], ], ], ]);
$node->children
ahora contiene una lista de nodos secundarios creados.
Puedes reconstruir fácilmente un árbol. Esto es útil para cambiar en masa la estructura del árbol.
Categoría::rebuildTree($datos, $eliminar);
$data
es una matriz de nodos:
$datos = [ [ 'id' => 1, 'nombre' => 'foo', 'niños' => [ ... ] ], ['nombre' => 'barra'], ];
Hay una identificación especificada para el nodo con el nombre foo
lo que significa que el nodo existente se completará y guardará. Si el nodo no existe, se lanza ModelNotFoundException
. Además, este nodo tiene children
especificados, que también son una matriz de nodos; serán procesados de la misma manera y guardados como hijos del nodo foo
.
bar
de nodos no tiene una clave principal especificada, por lo que se creará.
$delete
muestra si se deben eliminar nodos que ya existen pero que no están presentes en $data
. De forma predeterminada, los nodos no se eliminan.
A partir de 4.2.8 puedes reconstruir un subárbol:
Categoría::rebuildSubtree($raíz, $datos);
Esto limita la reconstrucción del árbol a los descendientes del nodo $root
.
En algunos casos usaremos una variable $id
que es una identificación del nodo de destino.
Los antepasados forman una cadena de padres hasta el nodo. Útil para mostrar rutas de navegación a la categoría actual.
Los descendientes son todos los nodos de un subárbol, es decir, hijos de nodos, hijos de hijos, etc.
Tanto los antepasados como los descendientes pueden cargarse con impaciencia.
// Accediendo a ancestros$nodo->ancestros;// Accediendo a descendientes$nodo->descendientes;
Es posible cargar antepasados y descendientes mediante una consulta personalizada:
$resultado = Categoría::ancestrosDe($id);$resultado = Categoría::ancestrosAndSelf($id);$resultado = Categoría::descendientesOf($id);$resultado = Categoría::descendientesAndSelf($id);
En la mayoría de los casos, necesitas que tus antepasados estén ordenados por nivel:
$resultado = Categoría::defaultOrder()->ancestrosOf($id);
Se puede cargar con entusiasmo una colección de antepasados:
$categorías = Categoría::with('ancestors')->paginate(30);// a la vista para rutas de navegación:@foreach($categorías as $i => $categoría) <pequeño>{{ $categoría->ancestros->count() ? implode(' > ', $categoría->ancestros->pluck('nombre')->toArray()) : 'Nivel superior' }}</small><br> {{ $categoría->nombre }} @endforeach
Los hermanos son nodos que tienen el mismo padre.
$resultado = $nodo->getSiblings();$resultado = $nodo->hermanos()->get();
Para obtener sólo los siguientes hermanos:
// Obtener un hermano que está inmediatamente después del nodo$resultado = $node->getNextSibling();// Obtener todos los hermanos que están después del nodo$resultado = $node->getNextSiblings();// Obtener todos los hermanos usando un consulta$resultado = $nodo->nextSiblings()->get();
Para obtener hermanos anteriores:
// Obtener un hermano que está inmediatamente antes del nodo$result = $node->getPrevSibling();// Obtener todos los hermanos que están antes del nodo$result = $node->getPrevSiblings();// Obtener todos los hermanos usando un consulta$resultado = $nodo->prevSiblings()->get();
Imaginemos que cada categoría has many
bienes. Es decir, se establece la relación HasMany
. ¿Cómo se pueden obtener todos los bienes de $category
y todos sus descendientes? ¡Fácil!
// Obtener identificadores de descendientes$categorías = $categoría->descendientes()->pluck('id');// Incluir el identificador de la categoría misma$categorías[] = $categoría->getKey();// Obtener bienes $bienes = Bienes::whereIn('category_id', $categorías)->get();
Si necesita saber en qué nivel está el nodo:
$resultado = Categoría::conProfundidad()->buscar($id);$profundidad = $resultado->profundidad;
El nodo raíz estará en el nivel 0. Los hijos de los nodos raíz tendrán un nivel de 1, etc.
Para obtener nodos de un nivel específico, puede aplicar having
restricción:
$resultado = Categoría::conProfundidad()->tener('profundidad', '=', 1)->get();
¡IMPORTANTE! Esto no funcionará en modo estricto de base de datos.
Todos los nodos están estrictamente organizados internamente. De forma predeterminada, no se aplica ningún orden, por lo que los nodos pueden aparecer en orden aleatorio y esto no afecta la visualización de un árbol. Puede ordenar los nodos por alfabeto u otro índice.
Pero en algunos casos el orden jerárquico es esencial. Es necesario para recuperar antepasados y se puede utilizar para ordenar elementos del menú.
Para aplicar el orden de los árboles se utiliza el método defaultOrder
:
$resultado = Categoría::defaultOrder()->get();
Puedes obtener nodos en orden inverso:
$resultado = Categoría::reversed()->get();
Para mover el nodo hacia arriba o hacia abajo dentro del padre para afectar el orden predeterminado:
$bool = $nodo->down();$bool = $nodo->up();// Cambiar el nodo por 3 hermanos$bool = $nodo->down(3);
El resultado de la operación es un valor booleano que indica si el nodo ha cambiado de posición.
Varias restricciones que se pueden aplicar al generador de consultas:
dondeIsRoot() para obtener sólo los nodos raíz;
hasParent() para obtener nodos no raíz;
dondeIsLeaf() para obtener sólo hojas;
hasChildren() para obtener nodos que no salen;
dondeIsAfter($id) para obtener todos los nodos (no solo los hermanos) que están detrás de un nodo con una identificación especificada;
dondeIsBefore($id) para obtener cada nodo que está antes de un nodo con una identificación especificada.
Restricciones de descendientes:
$resultado = Categoría::dondeDescendienteDe($nodo)->get();$resultado = Categoría::dondeNoDescendienteDe($nodo)->get();$resultado = Categoría::oDóndeDescendienteDe($nodo)->get() ;$resultado = Categoría::orWhereNotDescendantOf($nodo)->get();$resultado = Categoría::whereDescendantAndSelf($id)->get();// Incluir el nodo de destino en el conjunto de resultados$resultado = Categoría::dondeDescendenteOrSelf($nodo)->get();
Restricciones de los antepasados:
$resultado = Categoría::whereAncestorOf($nodo)->get();$resultado = Categoría::whereAncestorOrSelf($id)->get();
$node
puede ser una clave principal del modelo o una instancia del modelo.
Después de obtener un conjunto de nodos, puede convertirlo en árbol. Por ejemplo:
$árbol = Categoría::get()->toTree();
Esto completará las relaciones parent
e children
en cada nodo del conjunto y podrá representar un árbol usando un algoritmo recursivo:
$nodos = Categoría::get()->toTree();$traverse = función ($categorías, $prefijo = '-') use (&$traverse) {foreach ($categorías como $categoría) {echo PHP_EOL.$ prefijo.' '.$categoría->nombre;$traverse($categoría->niños, $prefijo.'-'); } };$traverse($nodos);
Esto generará algo como esto:
- Root -- Child 1 --- Sub child 1 -- Child 2 - Another root
Además, puede crear un árbol plano: una lista de nodos donde los nodos secundarios están inmediatamente después del nodo principal. Esto es útil cuando obtiene nodos con un orden personalizado (es decir, alfabéticamente) y no desea utilizar la recursividad para iterar sobre sus nodos.
$nodos = Categoría::get()->toFlatTree();
El ejemplo anterior generará:
Root Child 1 Sub child 1 Child 2 Another root
A veces no es necesario cargar el árbol completo y solo un subárbol de un nodo específico. Se muestra en el siguiente ejemplo:
$raíz = Categoría::descendientesAndSelf($rootId)->toTree()->first();
En una sola consulta obtenemos la raíz de un subárbol y todos sus descendientes a los que se puede acceder a través de una relación children
.
Si no necesita el nodo $root
, haga lo siguiente:
$árbol = Categoría::descendientesDe($rootId)->toTree($rootId);
Para eliminar un nodo:
$nodo->eliminar();
¡IMPORTANTE! ¡Cualquier descendiente que tenga ese nodo también será eliminado!
¡IMPORTANTE! Es necesario eliminar los nodos como modelos, no intentes eliminarlos usando una consulta como esta:
Categoría::where('id', '=', $id)->delete();
¡Esto romperá el árbol!
El rasgo SoftDeletes
es compatible, también a nivel de modelo.
Para comprobar si el nodo es descendiente de otro nodo:
$bool = $nodo->isDescendantOf($padre);
Para comprobar si el nodo es una raíz:
$bool = $nodo->isRoot();
Otros controles:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
Puedes comprobar si un árbol está roto (es decir, tiene algunos errores estructurales):
$bool = Categoría::estároto();
Es posible obtener estadísticas de errores:
$datos = Categoría::countErrors();
Devolverá una matriz con las siguientes claves:
oddness
: el número de nodos que tienen un conjunto incorrecto de valores lft
y rgt
duplicates
: el número de nodos que tienen los mismos valores lft
o rgt
wrong_parent
: el número de nodos que tienen un valor parent_id
no válido que no corresponde a los valores lft
y rgt
missing_parent
: el número de nodos que tienen parent_id
apuntando a un nodo que no existe.
Desde la versión 3.1, el árbol ahora se puede arreglar. Al utilizar la información de herencia de la columna parent_id
, se establecen los valores _lft
y _rgt
adecuados para cada nodo.
Nodo::fixTree();
Imagina que tienes Menu
model y MenuItems
. Existe una relación de uno a muchos entre estos modelos. MenuItem
tiene el atributo menu_id
para unir modelos. MenuItem
incorpora conjuntos anidados. Es obvio que querrás procesar cada árbol por separado según el atributo menu_id
. Para hacerlo, debe especificar este atributo como atributo de alcance:
función protegida getScopeAttributes() {regresar ['menu_id']; }
Pero ahora, para ejecutar alguna consulta personalizada, debe proporcionar atributos que se utilizan para determinar el alcance:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); // OKMenuItem::descendientesDe($id)->get(); // INCORRECTO: devuelve nodos de otro alcanceMenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // DE ACUERDO
Al solicitar nodos utilizando una instancia de modelo, los alcances se aplican automáticamente en función de los atributos de ese modelo:
$nodo = MenuItem::findOrFail($id);$nodo->hermanos()->withDepth()->get(); // DE ACUERDO
Para obtener el generador de consultas con ámbito mediante la instancia:
$nodo->newScopedQuery();
Utilice siempre una consulta con ámbito cuando se realice una carga ansiosa:
MenuItem::scoped([ 'menu_id' => 5])->with('descendientes')->findOrFail($id); // OKMenuItem::with('descendientes')->findOrFail($id); // EQUIVOCADO
PHP >= 5.4
Laravel >= 4.1
Se recomienda encarecidamente utilizar una base de datos que admita transacciones (como InnoDb de MySql) para proteger un árbol de una posible corrupción.
Para instalar el paquete, en la terminal:
composer require kalnoy/nestedset
Para usuarios de Laravel 5.5 y superiores:
Esquema::create('tabla', función (Blueprint $tabla) {...$tabla->nestedSet(); });// Para eliminar columnsSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
Para versiones anteriores de Laravel:
... utilizar KalnoyNestedsetNestedSet; Schema::create('tabla', función (Blueprint $tabla) {...NestedSet::columns($tabla); });
Para eliminar columnas:
... utilizar KalnoyNestedsetNestedSet; Esquema::tabla('tabla', función (Plano $tabla) { NestedSet::dropColumns($tabla); });
Su modelo debe usar el rasgo KalnoyNestedsetNodeTrait
para habilitar conjuntos anidados:
use KalnoyNestedsetNodeTrait; la clase Foo extiende el modelo {use NodeTrait; }
Si su extensión anterior usaba un conjunto diferente de columnas, solo necesita anular los siguientes métodos en su clase de modelo:
función pública getLftName() {regresar 'izquierda'; }función pública getRgtName() {regresar 'derecha'; }función pública getParentIdName() {regresar 'padre'; }// Especificar el atributo de identificación principal mutatorpublic function setParentAttribute($value) {$this->setParentIdAttribute($valor); }
Si su árbol contiene información parent_id
, debe agregar dos columnas a su esquema:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
Después de configurar su modelo, solo necesita arreglar el árbol para llenar las columnas _lft
y _rgt
:
MiModelo::fixTree();
Copyright (c) 2017 Alexander Kalnoy
Por el presente se otorga permiso, sin cargo, a cualquier persona que obtenga una copia de este software y los archivos de documentación asociados (el "Software"), para operar con el Software sin restricciones, incluidos, entre otros, los derechos de uso, copia, modificación, fusión. , publicar, distribuir, sublicenciar y/o vender copias del Software, y permitir que las personas a quienes se les proporciona el Software lo hagan, sujeto a las siguientes condiciones:
El aviso de derechos de autor anterior y este aviso de permiso se incluirán en todas las copias o partes sustanciales del Software.
EL SOFTWARE SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O IMPLÍCITA, INCLUYENDO, PERO NO LIMITADO A, LAS GARANTÍAS DE COMERCIABILIDAD, IDONEIDAD PARA UN PROPÓSITO PARTICULAR Y NO INFRACCIÓN. EN NINGÚN CASO LOS AUTORES O TITULARES DE DERECHOS DE AUTOR SERÁN RESPONSABLES DE NINGÚN RECLAMO, DAÑO U OTRA RESPONSABILIDAD, YA SEA EN UNA ACCIÓN CONTRACTUAL, AGRAVIO O DE OTRA MANERA, QUE SURJA DE, FUERA DE O EN RELACIÓN CON EL SOFTWARE O EL USO U OTRAS NEGOCIOS EN EL SOFTWARE.