Раньше тоже так делал (обычный AL и запросы к бд рекурсивно). На километровом дереве начались тормоза. Переделал на однозапросный вариант, как уже советовали. На практике оказалось, что тормоза были не из-за этого, а из-за рендрежки дерева. На сервере с настроенным mysql_cache и редко меняющейся таблице категорий разница между 1 и 200 запросами оказалась ерундовой, но она есть. + кому-то "повезёт" подождать дерево когда кэш сбросится.
Приложу свой вариант (как раз для элоквента), два класса, сложите куда-нибудь и переименуйте как вам надо.
class Tree
{
private $builder;
private $parentIdFieldName = 'parent_id';
private $nodesById = [];
private $nodesByParent = [];
public function __construct($builder)
{
$this->builder = $builder;
}
public function parentIdField($name)
{
$this->parentIdFieldName = $name;
return $this;
}
/**
* @return Output
*/
public function get()
{
$nodes = $this->builder->get();
foreach ($nodes as $node) {
$this->nodesById[$node->id] = $node;
$this->nodesByParent[$node->{$this->parentIdFieldName}][] = $node;
}
$output = new Output;
$output->parentIdFieldName = $this->parentIdFieldName;
$output->nodesById = $this->nodesById;
$output->nodesByParent = $this->nodesByParent;
return $output;
}
}
class Output
{
public $parentIdFieldName = 'parent_id';
public $nodesById = [];
public $nodesByParent = [];
public function getNode($id)
{
if (!isset($this->nodesById[$id])) {
$this->nodesById[$id] = [];
}
return $this->nodesById[$id];
}
public function getSubnodes($id)
{
if (!isset($this->nodesByParent[$id])) {
$this->nodesByParent[$id] = [];
}
return $this->nodesByParent[$id];
}
private $branch;
public function getBranch($id))
{
if (isset($this->nodesById[$id])) {
$this->branch = [];
$this->branchRecursion($id);
return array_reverse($this->branch);
}
}
private function branchRecursion($id))
{
$this->branch[] = $this->nodesById[$id];
if ($this->nodesById[$id][$this->parentIdFieldName] > 0) {
$this->branchRecursion($this->nodesById[$id][$this->parentIdFieldName]);
}
}
}
На вход надо подать билдер запроса, можно отфильтровать по каким-то критериям (видимость, например), отсортировать, приделать жадные загрузки и тд. Нельзя ограничивать по родительской категории, то есть никак нельзя таким способом взять поддерево определенного узла, это минус такого способа.
На выходе можно получить модель по ее ид ($tree->getNode($id)) и массив вложенных моделей модели с таким-то ид ($tree->getSubnodes($id)). Можно еще ветку получить - $tree->getBranch($id) выдаст массив моделей от корневой до той у которой ид = $id. Хлебные крошки рисовать чтобы, например
Вакуумный пример с вашей таблицей. Блейдом не пользовался, но думаю можно ему массив какой то дать или как там делается
class Test
{
private $tree;
private $output;
public function treeView()
{
$builder = new ProductCategory; // тут можно дописать что-нибудь типа orderBy('position') или where('enabled', true); главное так чтобы любые ограничения в первую очередь исключали вышестоящие узлы (ни в коем случае не исключали нижестоящие не исключая нижестоящие), иначе дерево получится кривое - недостаток способа
$this->tree = (new Tree($builder))->get();
$this->treeRecursion(0); // здесь уже можно указать ид корневого узла для дерева на выходе
return implode('<br>', $this->output);
}
private $level = 0;
private function treeViewRecursion($id)
{
$node = $this->tree->getNode($id);
$subnodes = $this->tree->getSubnodes($id);
$this->output[] = str_repeat('--', $this->level) . ' ' . $node->name;
if ($subnodes) {
$this->level++;
foreach ($subnodes as $subnode) {
$this->treeViewRecursion($subnode->id);
}
$this->level--;
}
}
}
print (new Test)->tree();
По-хорошему лучше вообще хранить хтмл кэш готового дерева и заменять его после каждой операции над ним