Пакет 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 — категории каталога: показ товаров из всех подкатегорий и построение хлебных крошек.