所有無限級分類的本質都是「id + parent_id」雙欄位:
parent_id = 0parent_id = 父分類 id適用於下拉選單、樹形選單、站點地圖等場景。
// 資料庫連接略
function displayTree($prefix, $parentId) {
$sql = "SELECT * FROM `ylmf_class` WHERE `parentid` = '$parentId'";
$res = mysql_query($sql);
while ($row = mysql_fetch_assoc($res)) {
echo $prefix . $row['classname'] . "
";
// 遞歸輸出子節點,縮進符號疊加
displayTree($prefix . "─┴", $row['id']);
}
}
// 呼叫:從頂級開始
displayTree("├", 0);
輸出效果:
├一級分類A ├─┴二級分類A ├─┴二級分類B ├一級分類B
當需要在後台列表中直觀展示「所屬關係」並支援行合併(rowspan)與列合併(colspan)時,純遞歸輸出 HTML <table> 會非常麻煩。TreeTable 類將數據預處理為座標系,再一次性輸出標準表格。
$data = [
1 => ['id'=>1, 'parentid'=>0, 'name'=>'一級欄目一'],
2 => ['id'=>2, 'parentid'=>0, 'name'=>'一級欄目二'],
3 => ['id'=>3, 'parentid'=>1, 'name'=>'二級欄目一'],
4 => ['id'=>4, 'parentid'=>1, 'name'=>'二級欄目二'],
5 => ['id'=>5, 'parentid'=>2, 'name'=>'二級欄目三'],
6 => ['id'=>6, 'parentid'=>3, 'name'=>'三級欄目一'],
7 => ['id'=>7, 'parentid'=>3, 'name'=>'三級欄目二'],
];
TreeTable(精簡版)class TreeTable {
public $arr = []; // 節點數據
public $columns = 0; // 最大層級數(列數)
public $rows = 0; // 底層節點數(行數)
public function init(array $data) {
// 1. 以 id 為鍵重組
foreach ($data as $v) $this->arr[$v['id']] = $v;
// 2. 計算每節點層級、所有子孫、所有祖先、底層葉子數
foreach ($this->arr as $k => $v) {
$this->arr[$k]['column'] = $this->getLevel($v['id']); // Y 軸:層級
$this->arr[$k]['arrchildid'] = $this->getArrChildId($v['id']); // 所有子孫 ID 串
$this->arr[$k]['arrparentid'] = $this->getArrParentId($v['id']); // 所有祖先 ID 串
$this->arr[$k]['child_bottom_num'] = $this->getChildCount($v['id']); // 葉子數
}
$this->columns = $this->getColumns();
$this->rows = $this->getRows();
// 3. 按祖先路徑 + id 排序,保證同層兄弟相鄰
$this->sortArr();
// 4. 計算每節點在表格中的行號、rowspan、colspan
foreach ($this->arr as $k => $v) {
$this->arr[$k]['row'] = $this->getRowLocation($v['id']);
$this->arr[$k]['rowspan'] = $v['child_bottom_num'];
$this->arr[$k]['colspan'] = $v['child_bottom_num'] == 0
? $this->columns - $v['column'] + 1 : 0;
}
return $this->arr;
}
// 以下為內部輔助方法:getLevel、getChildCount、getArrChildId、getArrParentId、getRowLocation、getRows、getColumns、sortArr
// (完整代碼見原文,此處省略以節省篇幅)
public function getTreTable() {
$html = '';
for ($r = 1; $r <= $this->rows; $r++) {
$html .= "";
foreach ($this->arr as $v) {
if ($v['row'] == $r) {
$rs = $v['rowspan'] ? "rowspan='{$v['rowspan']}'" : '';
$cs = $v['colspan'] ? "colspan='{$v['colspan']}'" : '';
$html .= "{$v['name']} ";
}
}
$html .= " ";
}
return $html;
}
}
$tree = new TreeTable(); $tree->init($data); echo "
渲染效果: 表格第一列顯示一級欄目,第二列顯示二級,第三列顯示三級;同一父節點的所有子孫自動合併儲存格,視覺上極其清晰。
| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 前端下拉選單、樹形選擇器 | 方案一(遞歸輸出) | 代碼極簡,配合 <optgroup> 或 JS 樹插件即可 |
| 後台列表頁、Excel 匯出、需行合併展示 | 方案二 | 自動計算 rowspan/colspan,輸出標準 HTML 表格,零前端邏輯 |
| 數據量 > 5000 節點 | 兩者皆不建議 | 改用 Closure Table 或資料庫遞歸 CTE(MySQL 8.0+) |
SELECT * FROM table 讀入記憶體,遞歸在 PHP 陣列中完成,避免 N+1 查詢。$tree->init($data) 結果序列化存入 Redis,下次直接反序列化輸出。parent_id,重建緩存即可,無需像 rank 方案那樣級聯更新路徑字段。根據業務場景靈活選擇,即可輕鬆駕馭任何層級的分類展示需求。