數據庫存儲 id, pid, name, ...,一次查詢取出所有數據,PHP 內存中構建樹結構,避免遞歸查詢數據庫(N+1 問題)。
class Tree {
private $data = [];
private $tree = [];
private $fields = ['id', 'pid'];
private $rootPid = 0;
public function __construct(array $data, array $fields = ['id', 'pid'], $rootPid = 0) {
$this->data = $data;
$this->fields = $fields;
$this->rootPid = $rootPid;
$this->build();
}
private function build() {
// 1. 按 pid 分組
$grouped = [];
foreach ($this->data as $node) {
$pid = $node[$this->fields[1]];
$grouped[$pid][] = $node;
}
krsort($grouped); // 確保父節點先處理(可選)
// 2. 遞歸構建子樹
$this->tree = $this->buildBranch($this->rootPid, $grouped);
}
private function buildBranch($pid, &$grouped) {
$branch = [];
if (empty($grouped[$pid])) return $branch;
foreach ($grouped[$pid] as $node) {
$child = $this->buildBranch($node[$this->fields[0]], $grouped);
if ($child) $node['children'] = $child;
$branch[] = $node;
}
return $branch;
}
/** 獲取某節點下的完整子樹(多維數組) */
public function getBranch($id = null) {
$id = $id ?? $this->rootPid;
// 需要先建立 id=>node 映射或遍歷查找,此處簡化
return $this->findBranch($this->tree, $id);
}
private function findBranch($branch, $id) {
foreach ($branch as $node) {
if ($node[$this->fields[0]] == $id) return $node['children'] ?? [];
if (!empty($node['children'])) {
$res = $this->findBranch($node['children'], $id);
if ($res) return $res;
}
}
return [];
}
/** 獲取從根到某節點的路徑(導航麵包屑) */
public function getPath($id) {
$path = [];
$this->buildPath($this->data, $id, $path);
return array_reverse($path);
}
private function buildPath($data, $id, &$path) {
foreach ($data as $node) {
if ($node[$this->fields[0]] == $id) {
$path[] = $node;
if ($node[$this->fields[1]] != $this->rootPid) {
$this->buildPath($data, $node[$this->fields[1]], $path);
}
break;
}
}
}
/** 獲取某節點下所有後代 ID(含自身) */
public function getAllDescendantIds($id) {
$ids = [$id];
$branch = $this->getBranch($id);
$this->collectIds($branch, $ids);
return $ids;
}
private function collectIds($branch, &$ids) {
foreach ($branch as $node) {
$ids[] = $node[$this->fields[0]];
if (!empty($node['children'])) $this->collectIds($node['children'], $ids);
}
}
}
$rawData = [
['id'=>1, 'pid'=>0, 'name'=>'首頁'],
['id'=>2, 'pid'=>0, 'name'=>'新聞'],
['id'=>6, 'pid'=>2, 'name'=>'國內新聞'],
['id'=>8, 'pid'=>6, 'name'=>'北京新聞'],
// ...
];
$tree = new Tree($rawData);
// 1. 完整樹(用於側邊欄菜單)
$menuTree = $tree->getBranch();
// 2. 麵包屑導航(當前分類 ID=8)
$breadcrumb = $tree->getPath(8);
// 3. 獲取分類 2 及所有子分類 ID(用於文章查詢 WHERE cat_id IN (...))
$catIds = $tree->getAllDescendantIds(2);
{* 模板文件 menu.tpl *}
{function name=renderMenu data=$menu}
{foreach $data as $node}
-
{$node.name}
{if isset($node.children)}
{call name=renderMenu data=$node.children}
{/if}
{/foreach}
{/function}
{* 調用 *}
{call name=renderMenu data=$menuTree}
$tree 序列化存入 Redis/文件,命中緩存直接反序列化。getChildren($pid) 接口。path 字段存儲 0/2/6/8/,查詢子樹變 WHERE path LIKE '0/2/%',極快。