Este é um pacote Laravel 4-10 para trabalhar com árvores em bancos de dados relacionais.
Laravel 11.0 é suportado desde a v6.0.4
Laravel 10.0 é suportado desde a v6.0.2
Laravel 9.0 é suportado desde v6.0.1
Laravel 8.0 é suportado desde a v6.0.0
Laravel 5.7, 5.8, 6.0, 7.0 é suportado desde a v5
Laravel 5.5, 5.6 é suportado desde a v4.3
Laravel 5.2, 5.3, 5.4 é suportado desde a v4
Laravel 5.1 é suportado na v3
Laravel 4 é suportado na v2
Conteúdo:
Teoria
Documentação
Inserindo nós
Recuperando nós
Excluindo nós
Verificação e correção de consistência
Escopo
Requisitos
Instalação
Conjuntos aninhados ou modelo de conjunto aninhado são uma maneira de armazenar dados hierárquicos com eficácia em uma tabela relacional. Da Wikipédia:
O modelo de conjunto aninhado consiste em numerar os nós de acordo com um percurso em árvore, que visita cada nó duas vezes, atribuindo números na ordem de visita e em ambas as visitas. Isso deixa dois números para cada nó, que são armazenados como dois atributos. A consulta torna-se barata: a participação na hierarquia pode ser testada comparando esses números. A atualização requer renumeração e, portanto, é cara.
O NSM mostra bom desempenho quando a árvore raramente é atualizada. Ele foi ajustado para ser rápido na obtenção de nós relacionados. É ideal para criar menus ou categorias multiprofundas para compras.
Suponha que temos um modelo Category
; uma variável $node
é uma instância desse modelo e do nó que estamos manipulando. Pode ser um modelo novo ou do banco de dados.
O nó possui os seguintes relacionamentos que são totalmente funcionais e podem ser carregados avidamente:
O nó pertence ao parent
Node tem muitos children
Node tem muitos ancestors
Node tem muitos descendants
Mover e inserir nós inclui diversas consultas ao banco de dados, por isso é altamente recomendável usar transações.
IMPORTANTE! A partir da versão 4.2.0, a transação não é iniciada automaticamente
Outra observação importante é que as manipulações estruturais são adiadas até que você clique em save
no modelo (alguns métodos chamam implicitamente save
e retornam o resultado booleano da operação).
Se o modelo for salvo com sucesso, isso não significa que o nó foi movido. Se a sua aplicação depende se o nó realmente mudou de posição, use o método hasMoved
:
if ($node->save()) {$moved = $node->hasMoved(); }
Quando você simplesmente cria um nó, ele será anexado ao final da árvore:
Categoria::criar($atributos); //Salvo como root
$node = new Categoria($atributos);$node->save(); //Salvo como root
Neste caso o nó é considerado raiz , o que significa que não possui pai.
// #1 save$node->saveAsRoot() implícito;// #2 save$node->makeRoot()->save();
O nó será anexado ao final da árvore.
Se você quiser tornar o nó filho de outro nó, poderá torná-lo o último ou o primeiro filho.
Nos exemplos a seguir, $parent
é algum nó existente.
Existem algumas maneiras de anexar um nó:
// #1 Usando inserção adiada$node->appendToNode($parent)->save();// #2 Usando o nó pai$parent->appendNode($node);// #3 Usando o relacionamento dos filhos dos pais$parent- >children()->create($attributes);// #5 Usando o relacionamento pai do nó$node->parent()->associate($parent)->save();// #6 Usando o pai atributo$node->parent_id = $parent->id;$node->save();// #7 Usando método estáticoCategory::create($attributes, $parent);
E apenas algumas maneiras de acrescentar:
// #1$node->prependToNode($parent)->save();// #2$parent->prependNode($node);
Você pode fazer com que $node
seja vizinho do nó $neighbor
usando os seguintes métodos:
$neighbor
deve existir, o nó de destino pode ser novo. Se o nó de destino existir, ele será movido para a nova posição e o pai será alterado se for necessário.
# Salvamento explícito$node->afterNode($neighbor)->save();$node->beforeNode($neighbor)->save();# Salvamento implícito$node->insertAfterNode($neighbor);$node-> insertBeforeNode($vizinho);
Ao usar o método estático create
no nó, ele verifica se os atributos contêm chave children
. Se isso acontecer, ele criará mais nós recursivamente.
$node = Categoria::create(['nome' => 'Foo','crianças' => [ ['nome' => 'Barra','crianças' => [ [ 'nome' => 'Baz'], ], ], ], ]);
$node->children
agora contém uma lista de nós filhos criados.
Você pode facilmente reconstruir uma árvore. Isso é útil para alterar em massa a estrutura da árvore.
Categoria::rebuildTree($dados, $delete);
$data
é uma matriz de nós:
$ dados = [ [ 'id' => 1, 'nome' => 'foo', 'filhos' => [ ... ] ], [ 'nome' => 'barra'], ];
Há um id especificado para o nó com o nome foo
o que significa que o nó existente será preenchido e salvo. Se o nó não existir, ModelNotFoundException
será lançada. Além disso, este nó possui children
especificados, que também são uma matriz de nós; eles serão processados da mesma maneira e salvos como filhos do nó foo
.
bar
de nós não possui chave primária especificada, portanto será criada.
$delete
mostra se devem ser excluídos nós que já existem, mas não estão presentes em $data
. Por padrão, os nós não são excluídos.
A partir da versão 4.2.8 você pode reconstruir uma subárvore:
Categoria::rebuildSubtree($root, $data);
Isso restringe a reconstrução da árvore aos descendentes do nó $root
.
Em alguns casos usaremos uma variável $id
que é um id do nó de destino.
Os ancestrais formam uma cadeia de pais até o nó. Útil para exibir trilhas para a categoria atual.
Descendentes são todos os nós de uma subárvore, ou seja, filhos do nó, filhos dos filhos, etc.
Tanto os ancestrais quanto os descendentes podem ser carregados avidamente.
// Acessando ancestrais$node->ancestors;// Acessando descendentes$node->descendants;
É possível carregar ancestrais e descendentes usando consulta personalizada:
$resultado = Categoria::ancestorsOf($id);$resultado = Categoria::ancestorsAndSelf($id);$resultado = Categoria::descendentesOf($id);$resultado = Categoria::descendentesAndSelf($id);
Na maioria dos casos, você precisa que seus ancestrais sejam ordenados por nível:
$resultado = Categoria::defaultOrder()->ancestorsOf($id);
Uma coleção de ancestrais pode ser carregada avidamente:
$categories = Category::with('ancestors')->paginate(30);// na visualização para breadcrumbs:@foreach($categories as $i => $category) <small>{{ $category->ancestors->count() ? implode(' > ', $category->ancestors->pluck('name')->toArray()) : 'Top Level' }}</small><br> {{ $categoria->nome }} @endforeach
Irmãos são nós que possuem o mesmo pai.
$resultado = $node->getSiblings();$resultado = $node->siblings()->get();
Para obter apenas os próximos irmãos:
// Obtém um irmão que está imediatamente após o node$result = $node->getNextSibling();// Obtém todos os irmãos que estão após o node$result = $node->getNextSiblings();// Obtém todos os irmãos usando um query$result = $node->nextSiblings()->get();
Para obter irmãos anteriores:
// Obtém um irmão que está imediatamente antes do node$result = $node->getPrevSibling();// Obtém todos os irmãos que estão antes do node$result = $node->getPrevSiblings();// Obtém todos os irmãos usando um query$resultado = $node->prevSiblings()->get();
Imagine que cada categoria has many
produtos. Ou seja, o relacionamento HasMany
é estabelecido. Como você pode obter todos os bens da $category
e todos os seus descendentes? Fácil!
// Obtém os ids dos descendentes$categories = $category->descendants()->pluck('id');// Inclui o id da própria categoria$categories[] = $category->getKey();// Obtém os bens $mercadorias = Mercadorias::whereIn('category_id', $categories)->get();
Se você precisa saber em qual nível o nó está:
$resultado = Categoria::comProfundidade()->find($id);$profundidade = $resultado->profundidade;
O nó raiz estará no nível 0. Os filhos dos nós raiz terão o nível 1, etc.
Para obter nós de nível especificado, você pode aplicar having
:
$resultado = Categoria::comProfundidade()->tendo('profundidade', '=', 1)->get();
IMPORTANTE! Isso não funcionará no modo estrito do banco de dados
Todos os nós são estritamente organizados internamente. Por padrão, nenhuma ordem é aplicada, portanto os nós podem aparecer em ordem aleatória e isso não afeta a exibição de uma árvore. Você pode ordenar os nós por alfabeto ou outro índice.
Mas em alguns casos a ordem hierárquica é essencial. É necessário para recuperar ancestrais e pode ser usado para solicitar itens de menu.
Para aplicar a ordem da árvore, o método defaultOrder
é usado:
$resultado = Categoria::defaultOrder()->get();
Você pode obter nós na ordem inversa:
$resultado = Categoria::reversed()->get();
Para mudar o nó para cima ou para baixo dentro do pai para afetar a ordem padrão:
$bool = $node->down();$bool = $node->up();// Desloca o nó em 3 irmãos$bool = $node->down(3);
O resultado da operação é um valor booleano que indica se o nó mudou de posição.
Várias restrições que podem ser aplicadas ao construtor de consultas:
whereIsRoot() para obter apenas nós raiz;
hasParent() para obter nós não raiz;
whereIsLeaf() para obter apenas folhas;
hasChildren() para obter nós que não são de saída;
whereIsAfter($id) para obter todos os nós (não apenas irmãos) que estão atrás de um nó com id especificado;
whereIsBefore($id) para obter cada nó que está antes de um nó com id especificado.
Restrições de descendentes:
$resultado = Categoria::whereDescendantOf($node)->get();$resultado = Categoria::whereNotDescendantOf($node)->get();$resultado = Categoria::orWhereDescendantOf($node)->get() ;$resultado = Categoria::orWhereNotDescendantOf($node)->get();$resultado = Category::whereDescendantAndSelf($id)->get();// Inclui o nó de destino no conjunto de resultados$result = Category::whereDescendantOrSelf($node)->get();
Restrições ancestrais:
$resultado = Categoria::whereAncestorOf($node)->get();$resultado = Categoria::whereAncestorOrSelf($id)->get();
$node
pode ser uma chave primária do modelo ou uma instância do modelo.
Depois de obter um conjunto de nós, você pode convertê-lo em árvore. Por exemplo:
$árvore = Categoria::get()->toTree();
Isso preencherá os relacionamentos parent
e children
em cada nó do conjunto e você poderá renderizar uma árvore usando algoritmo recursivo:
$nodes = Categoria::get()->toTree();$traverse = function ($categories, $prefix = '-') use (&$traverse) {foreach ($categories as $category) {echo PHP_EOL.$ prefixo.' '.$categoria->nome;$traverse($categoria->filhos, $prefixo.'-'); } };$traverse($nós);
Isso produzirá algo assim:
- Root -- Child 1 --- Sub child 1 -- Child 2 - Another root
Além disso, você pode construir uma árvore plana: uma lista de nós onde os nós filhos estão imediatamente após o nó pai. Isso é útil quando você obtém nós com ordem personalizada (ou seja, em ordem alfabética) e não deseja usar recursão para iterar sobre seus nós.
$nodes = Categoria::get()->toFlatTree();
O exemplo anterior produzirá:
Root Child 1 Sub child 1 Child 2 Another root
Às vezes você não precisa carregar a árvore inteira e apenas alguma subárvore de um nó específico. É mostrado no exemplo a seguir:
$root = Categoria::descendentesAndSelf($rootId)->toTree()->first();
Em uma única consulta obtemos a raiz de uma subárvore e todos os seus descendentes que são acessíveis através da relação children
.
Se você não precisa do próprio nó $root
, faça o seguinte:
$árvore = Categoria::descendentesOf($rootId)->toTree($rootId);
Para excluir um nó:
$node->delete();
IMPORTANTE! Qualquer descendente desse nó também será excluído!
IMPORTANTE! Os nós devem ser excluídos como modelos, não tente excluí-los usando uma consulta como esta:
Categoria::where('id', '=', $id)->delete();
Isso vai quebrar a árvore!
A característica SoftDeletes
é suportada, também no nível do modelo.
Para verificar se o nó é descendente de outro nó:
$bool = $node->isDescendantOf($parent);
Para verificar se o nó é raiz:
$bool = $node->isRoot();
Outras verificações:
$node->isChildOf($other);
$node->isAncestorOf($other);
$node->isSiblingOf($other);
$node->isLeaf()
Você pode verificar se uma árvore está quebrada (ou seja, tem alguns erros estruturais):
$bool = Categoria::isBroken();
É possível obter estatísticas de erro:
$dados = Categoria::countErrors();
Ele retornará um array com as seguintes chaves:
oddness
- o número de nós que possuem conjuntos errados de valores lft
e rgt
duplicates
- o número de nós que possuem os mesmos valores lft
ou rgt
wrong_parent
- o número de nós que possuem valor parent_id
inválido que não corresponde aos valores lft
e rgt
missing_parent
– o número de nós que possuem parent_id
apontando para um nó que não existe
Desde a versão 3.1, a árvore agora pode ser corrigida. Usando informações de herança da coluna parent_id
, os valores _lft
e _rgt
adequados são definidos para cada nó.
Node::fixTree();
Imagine que você tem Menu
model e MenuItems
. Existe um relacionamento um-para-muitos estabelecido entre esses modelos. MenuItem
possui o atributo menu_id
para unir modelos. MenuItem
incorpora conjuntos aninhados. É óbvio que você deseja processar cada árvore separadamente com base no atributo menu_id
. Para fazer isso, você precisa especificar este atributo como atributo de escopo:
função protegida getScopeAttributes() {retornar ['menu_id']; }
Mas agora, para executar alguma consulta personalizada, você precisa fornecer atributos que serão usados para definição do escopo:
MenuItem::scoped([ 'menu_id' => 5 ])->withDepth()->get(); //OKMenuItem::descendentesOf($id)->get(); // ERRADO: retorna nós de outro scopeMenuItem::scoped([ 'menu_id' => 5 ])->fixTree(); // OK
Ao solicitar nós usando instância de modelo, os escopos são aplicados automaticamente com base nos atributos desse modelo:
$node = MenuItem::findOrFail($id);$node->siblings()->withDepth()->get(); // OK
Para obter o construtor de consultas com escopo usando instância:
$node->newScopedQuery();
Sempre use consulta com escopo ao carregar antecipadamente:
MenuItem::scoped([ 'menu_id' => 5])->with('descendentes')->findOrFail($id); // OKMenuItem::with('descendentes')->findOrFail($id); // ERRADO
PHP >= 5.4
Laravel >= 4.1
É altamente recomendável usar banco de dados que suporte transações (como o InnoDb do MySql) para proteger uma árvore de possível corrupção.
Para instalar o pacote, no terminal:
composer require kalnoy/nestedset
Para usuários do Laravel 5.5 e superior:
Esquema::create('tabela', function (Blueprint $tabela) {...$tabela->nestedSet(); });// Para eliminar colunasSchema::table('table', function (Blueprint $table) {$table->dropNestedSet(); });
Para versões anteriores do Laravel:
...use KalnoyNestedsetNestedSet; Schema::create('tabela', function (Blueprint $tabela) {...NestedSet::columns($tabela); });
Para eliminar colunas:
...use KalnoyNestedsetNestedSet; Schema::table('tabela', function (Blueprint $tabela) { NestedSet::dropColumns($tabela); });
Seu modelo deve usar a característica KalnoyNestedsetNodeTrait
para habilitar conjuntos aninhados:
use KalnoyNestedsetNodeTrait;class Foo estende o modelo {use NodeTrait; }
Se sua extensão anterior usava um conjunto diferente de colunas, você só precisa substituir os seguintes métodos em sua classe de modelo:
função pública getLftName() {retornar 'esquerda'; }função pública getRgtName() {retornar 'certo'; }função pública getParentIdName() {retornar 'pai'; }// Especifique o atributo de id pai mutatorpublic function setParentAttribute($value) {$this->setParentIdAttribute($valor); }
Se sua árvore contém informações parent_id
, você precisa adicionar duas colunas ao seu esquema:
$table->unsignedInteger('_lft');$table->unsignedInteger('_rgt');
Depois de configurar seu modelo você só precisa corrigir a árvore para preencher as colunas _lft
e _rgt
:
MeuModelo::fixTree();
Direitos autorais (c) 2017 Alexander Kalnoy
É concedida permissão, gratuitamente, a qualquer pessoa que obtenha uma cópia deste software e dos arquivos de documentação associados (o "Software"), para negociar o Software sem restrições, incluindo, sem limitação, os direitos de usar, copiar, modificar, mesclar , publicar, distribuir, sublicenciar e/ou vender cópias do Software e permitir que as pessoas a quem o Software seja fornecido o façam, sujeito às seguintes condições:
O aviso de direitos autorais acima e este aviso de permissão serão incluídos em todas as cópias ou partes substanciais do Software.
O SOFTWARE É FORNECIDO "COMO ESTÁ", SEM GARANTIA DE QUALQUER TIPO, EXPRESSA OU IMPLÍCITA, INCLUINDO, MAS NÃO SE LIMITANDO ÀS GARANTIAS DE COMERCIALIZAÇÃO, ADEQUAÇÃO A UM DETERMINADO FIM E NÃO VIOLAÇÃO. EM HIPÓTESE ALGUMA OS AUTORES OU DETENTORES DE DIREITOS AUTORAIS SERÃO RESPONSÁVEIS POR QUALQUER RECLAMAÇÃO, DANOS OU OUTRA RESPONSABILIDADE, SEJA EM UMA AÇÃO DE CONTRATO, ATO ILÍCITO OU DE OUTRA FORMA, DECORRENTE DE, OU EM CONEXÃO COM O SOFTWARE OU O USO OU OUTRAS NEGOCIAÇÕES NO SOFTWARE.