Пакет staudenmeir/laravel-adjacency-list
Glossary overview

Пакет staudenmeir/laravel-adjacency-list

Пакет staudenmeir/laravel-adjacency-list добавляет в Laravel рекурсивные связи для древовидных структур (категории, комментарии, меню). Он использует Common Table Expressions (CTE) — рекурсивные SQL-запросы, которые поддерживаются в MySQL 8+, PostgreSQL, SQLite 3.8.3+ и SQL Server.

Главное преимущество — для хранения дерева нужна только одна колонка parent_id. Никаких lft, rgt, depth в таблице — всё вычисляется на лету через CTE.

Установка

composer require staudenmeir/laravel-adjacency-list

Структура таблицы

Таблица категорий использует паттерн adjacency list — каждая запись ссылается на своего родителя через parent_id:

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
    $table->string('name');
    $table->string('slug')->unique();
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Корневые категории имеют parent_id = null. Дочерние ссылаются на ID родителя.

Подключение в модели

Достаточно подключить трейт HasRecursiveRelationships:

use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

class Category extends Model
{
    use HasRecursiveRelationships;

    protected $fillable = ['parent_id', 'name', 'slug', 'is_active'];
}

После этого модель получает набор рекурсивных связей: descendants(), ancestors(), ancestorsAndSelf(), descendantsAndSelf(), bloodline(), siblings() и другие.

Метод descendants() — что делает

Метод descendants() возвращает всех потомков категории на любую глубину вложенности — детей, внуков, правнуков и так далее. Это рекурсивная связь (relationship), которую можно использовать как обычную Eloquent-связь.

Например, если структура категорий такая:

Электроника (id: 1)
├── Смартфоны (id: 2, parent_id: 1)
│   ├── Apple (id: 4, parent_id: 2)
│   └── Samsung (id: 5, parent_id: 2)
└── Ноутбуки (id: 3, parent_id: 1)

Тогда Category::find(1)->descendants вернёт коллекцию: Смартфоны, Ноутбуки, Apple, Samsung — все 4 потомка, на любой глубине.

Вот пример:

$category->descendants()

Как это работает под капотом (CTE)

Пакет генерирует рекурсивный SQL-запрос с WITH RECURSIVE. Для descendants() категории с ID = 1 запрос выглядит примерно так:

WITH RECURSIVE laravel_cte AS (
    -- Базовый случай: прямые дети
    SELECT *, 1 AS depth, CAST(id AS CHAR(65536)) AS path
    FROM categories
    WHERE parent_id = 1

    UNION ALL

    -- Рекурсивный шаг: дети детей
    SELECT c.*, laravel_cte.depth + 1, CONCAT(laravel_cte.path, '.', c.id)
    FROM categories c
    INNER JOIN laravel_cte ON c.parent_id = laravel_cte.id
)
SELECT * FROM laravel_cte

Запрос автоматически добавляет два виртуальных столбца: depth (глубина вложенности от стартового узла) и path (путь из ID через точку, например 1.2.4).

Практические примеры использования

1. Получить ID всех потомков для фильтрации товаров:

// Собираем ID текущей категории + всех её потомков
$categoryIds = $category->descendants()
    ->where('is_active', true)
    ->pluck('id')
    ->prepend($category->id);

// Ищем товары во всех этих категориях
$products = Product::whereIn('category_id', $categoryIds)
    ->where('is_active', true)
    ->paginate(12);

Это позволяет при открытии родительской категории «Электроника» показать товары из всех вложенных подкатегорий.

2. Хлебные крошки через ancestorsAndSelf():

// Получаем всех предков + текущую категорию, разворачиваем от корня
$breadcrumbs = $category->ancestorsAndSelf()->get()->reverse();

// Результат: Электроника → Смартфоны → Apple

3. Ограничение по глубине:

// Только прямые потомки и их дети (2 уровня)
$descendants = $category->descendants()
    ->whereDepth('<=', 2)
    ->get();

4. Сортировка по дереву:

// Сортировка в порядке обхода дерева (родитель перед детьми)
$tree = $category->descendants()->depthFirst()->get();

// Или в ширину (все узлы одного уровня вместе)
$tree = $category->descendants()->breadthFirst()->get();

Все доступные рекурсивные связи

После подключения трейта модель получает следующие методы:

$category->descendants();        // Все потомки
$category->descendantsAndSelf(); // Все потомки + текущий узел
$category->ancestors();          // Все предки
$category->ancestorsAndSelf();   // Все предки + текущий узел
$category->parent();             // Прямой родитель
$category->children();           // Прямые дети
$category->siblings();           // Все на том же уровне
$category->bloodline();          // Предки + потомки + сам узел
$category->rootAncestor();       // Корневой предок

Итого

Пакет staudenmeir/laravel-adjacency-list — простой способ работать с деревьями в Laravel. Достаточно одной колонки parent_id и одного трейта. Рекурсивные CTE-запросы выполняются за один проход к базе, без необходимости хранить дополнительные колонки как в nested set. Основной use-case — категории каталога: показ товаров из всех подкатегорий и построение хлебных крошек.