電商、CMS 專案常需呈現「二級分類」結構:一級分類下包含多個子分類。ThinkPHP 的 ORM 配合巢狀迴圈可輕鬆完成。本文示範以單一查詢取得所有一級分類,再逐筆查詢子分類並掛載至 child 鍵,最終輸出巢狀陣列供模板直接渲染。
<?php
// 控制器動作
public function categoryList() {
$Category = D('Category');
// 1. 取得所有一級分類 (parent_id = 0)
$parentList = $Category->where('category_pid = 0')->select();
if (!empty($parentList)) {
foreach ($parentList as $key => $val) {
$pid = $val['category_id'];
// 2. 查詢該父分類下的子分類
$children = $Category->where("category_pid = $pid")->select();
// 3. 掛載至 child 鍵
$parentList[$key]['child'] = $children;
// 除錯:列印最後執行的 SQL
// echo $Category->getLastSql();
}
}
// 4. 指派給模板
$this->assign('Category', $parentList);
$this->display();
}
?>
<!-- 模板檔案 -->
<ul class="category-tree">
<volist name="Category" id="cat">
<li>
<a href="{:U('Article/index', ['cid' => $cat.category_id])}">
{$cat.category_name}
</a>
<if condition="!empty($cat.child)">
<ul class="sub-category">
<volist name="cat.child" id="sub">
<li>
<a href="{:U('Article/index', ['cid' => $sub.category_id])}">
{$sub.category_name}
</a>
</li>
</volist>
</ul>
</if>
</li>
</volist>
</ul>
| 問題 | 解決方案 | 備註 |
|---|---|---|
| N+1 查詢:1 筆父 + N 筆子 = N+1 次 SQL | 一次撈全部分類,PHP 端組裝樹狀結構 | 適用分類總數 < 500 筆 |
| 分類超過千筆 | 資料庫加 path 欄位 (如 001.003.012) + 遞迴 CTE / 遞迴函數 | MySQL 8.0+ 支援 WITH RECURSIVE |
| 頻繁讀取、極少異動 | 快取完整樹狀 JSON 至 Redis / 檔案,TTL 1 小時 | 讀取效能 O(1) |
| 需支援無限層級 | 改用遞迴函數 buildTree($pid = 0, $level = 0) | 配合 level 控制縮排 |
<?php
$all = D('Category')->order('category_pid, sort')->select();
// 索引陣列:id => node
$map = [];
foreach ($all as $item) {
$map[$item['category_id']] = $item;
$map[$item['category_id']]['child'] = [];
}
// 組裝父子關聯
$tree = [];
foreach ($all as $item) {
$pid = $item['category_pid'];
if ($pid == 0) {
$tree[] = &$map[$item['category_id']];
} elseif (isset($map[$pid])) {
$map[$pid]['child'][] = &$map[$item['category_id']];
}
}
// $tree 即為完整巢狀結構,僅 1 次 SQL
?>
ThinkPHP 兩層分類讀取的基本寫法簡單易懂,適合中小型專案。隨著資料量增長,建議改用「單次查詢 + 陣列組裝」或「資料庫路徑編碼 + 遞迴 CTE」搭配快取策略,兼顧開發效率與執行效能。