在 Web 開發中,商品分類、文章分類、評論回覆等場景常需要支援「無限級分類」(Infinite Category / 無限分類)。本文將詳細介紹如何使用 PHP 搭配 MySQL 資料庫,實作一套完整的無限分類系統,包含新增、刪除、編輯、移動節點等核心功能,並提供資料表結構與前台調用範例,方便直接套用至專案中。
最常見且簡單的做法是在資料表中加入 f_id(父級 ID)欄位,指向同一張表的主鍵 id。頂層節點的 f_id 設為 0 或 NULL。
CREATE TABLE IF NOT EXISTS `p_newsclass` (
`id` INT(7) NOT NULL AUTO_INCREMENT,
`f_id` INT(7) NOT NULL DEFAULT 0 COMMENT '父級分類 ID',
`name` VARCHAR(255) NOT NULL COMMENT '分類名稱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=10;
INSERT INTO `p_newsclass` (`id`, `f_id`, `name`) VALUES
(3, 0, '中國'),
(4, 3, '福建'),
(5, 4, '龍巖市'),
(7, 4, '廈門市'),
(9, 5, '漳平市');
以下類別封裝了分類樹的建構、遍歷、層級計算、CRUD 及節點移動等操作。程式碼已修正原始版本中的語法錯誤(如 emptyempty → empty、$_post → $_POST 等),並改用 mysqli 預處理語句提升安全性。
<?php
class SortClass {
private $data = []; // id => name
private $child = []; // parent_id => [child_ids]
private $layer = []; // id => depth
private $parent = []; // id => parent_id
private $link;
private $table;
public function __construct(mysqli $link, string $table) {
$this->link = $link;
$this->table = $table;
$this->setNode(0, -1, '頂極節點'); // 虛擬根節點
$sql = "SELECT `id`, `f_id`, `name` FROM `{$this->table}`";
$res = $this->link->query($sql);
while ($row = $res->fetch_assoc()) {
$this->setNode($row['id'], $row['f_id'], $row['name']);
}
}
private function setNode(int $id, int $parent, string $value): void {
$parent = $parent ?? 0;
$this->data[$id] = $value;
$this->child[$id] = [];
$this->child[$parent][] = $id;
$this->parent[$id] = $parent;
$this->layer[$id] = isset($this->layer[$parent]) ? $this->layer[$parent] + 1 : 0;
}
/** 遞迴取得指定節點下所有子孫 ID(含自身) */
public function getChilds(int $id = 0): array {
$list = [$id];
$this->getList($list, $id);
return $list;
}
private function getList(array &$tree, int $root): void {
foreach ($this->child[$root] as $id) {
$tree[] = $id;
if (!empty($this->child[$id])) {
$this->getList($tree, $id);
}
}
}
/** 取得節點名稱 */
public function getValue(int $id): string {
return $this->data[$id] ?? '';
}
/** 取得節點層級,可選擇回傳縮排字串 */
public function getLayer(int $id, string $space = ''): string|int {
return $space ? str_repeat($space, $this->layer[$id]) : $this->layer[$id];
}
/** 取得直接父節點 ID */
public function getParent(int $id): int {
return $this->parent[$id] ?? -1;
}
/** 取得所有祖先節點 ID 陣列(由根至父) */
public function getParents(int $id): array {
$parents = [];
while (($pid = $this->parent[$id] ?? -1) !== -1) {
$parents[] = $pid;
$id = $pid;
}
return array_reverse($parents);
}
/** 取得直接子節點 ID 陣列 */
public function getChild(int $id): array {
return $this->child[$id] ?? [];
}
// ---------- CRUD 操作 ----------
public function addNode(string $name, int $pid): bool {
$stmt = $this->link->prepare("INSERT INTO `{$this->table}` (`f_id`, `name`) VALUES (?, ?)");
$stmt->bind_param('is', $pid, $name);
return $stmt->execute();
}
public function modNode(int $cid, string $newName): bool {
$stmt = $this->link->prepare("UPDATE `{$this->table}` SET `name` = ? WHERE `id` = ?");
$stmt->bind_param('si', $newName, $cid);
return $stmt->execute();
}
public function delNode(int $cid): bool {
$allChilds = $this->getChilds($cid);
$ids = array_merge([$cid], $allChilds);
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $this->link->prepare("DELETE FROM `{$this->table}` WHERE `id` IN ($placeholders)");
$types = str_repeat('i', count($ids));
$stmt->bind_param($types, ...$ids);
return $stmt->execute();
}
public function moveNode(int $cid, int $topId): bool {
$stmt = $this->link->prepare("UPDATE `{$this->table}` SET `f_id` = ? WHERE `id` = ?");
$stmt->bind_param('ii', $topId, $cid);
return $stmt->execute();
}
}
?>
搭配簡單的 HTML 表單與 JavaScript,即可完成後台管理介面。下方程式碼示範如何產生巢狀下拉選單、顯示分類樹、處理表單提交。
<?php
// 連線資料庫(建議改用 mysqli)
$link = new mysqli('localhost', 'root', '', 'class');
$link->set_charset('utf8');
require_once 'SortClass.php';
$tree = new SortClass($link, 'p_newsclass');
// 處理表單動作
$op = $_REQUEST['op'] ?? '';
if ($op) {
switch ($op) {
case 'add':
$tree->addNode($_POST['cname'], (int)$_POST['pid']);
break;
case 'mod':
$tree->modNode((int)$_POST['cid'], $_POST['cname']);
break;
case 'del':
$tree->delNode((int)$_GET['cid']);
break;
case 'move':
$tree->moveNode((int)$_POST['who'], (int)$_POST['to']);
break;
}
// 重導向避免重複提交
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
// 取得完整分類樹(含虛擬根節點 0 的所有子孫)
$category = $tree->getChilds(0);
// 產生下拉選單的輔助函式
function makeSelect(array $ids, SortClass $tree, string $name): string {
$html = '<select name="' . $name . '">';
foreach ($ids as $id) {
if ($id === 0) continue; // 跳過虛擬根節點
$indent = $tree->getLayer($id, '|— ');
$html .= '<option value="' . $id . '">' . htmlspecialchars($indent . $tree->getValue($id)) . '</option>';
}
return $html . '</select>';
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>無限分類管理示範</title>
<style>
body {font-family: sans-serif; font-size: 14px;}
ul {list-style: none; padding-left: 1.5em;}
li {margin: 4px 0;}
a {cursor: pointer; color: #0066cc; text-decoration: none;}
a:hover {text-decoration: underline;}
</style>
</head>
<body>
<h2>新增分類</h2>
<form method="post">
名稱:<input type="text" name="cname" required>
所屬分類:= makeSelect($category, $tree, 'pid') ?>
<input type="hidden" name="op" value="add">
<button type="submit">送出</button>
</form>
<h2>移動分類</h2>
<form method="post">
選擇分類:= makeSelect($category, $tree, 'who') ?>
移動到:= makeSelect($category, $tree, 'to') ?>
<input type="hidden" name="op" value="move">
<button type="submit">執行移動</button>
</form>
<h2>分類列表</h2>
<ul>
<?php foreach ($category as $id): if ($id === 0) continue; ?>
<li>
= $tree->getLayer($id, '|— ') . htmlspecialchars($tree->getValue($id)) ?>
<a href="?op=del&cid== $id ?>" onclick="return confirm('確定刪除?')">刪除</a>
<a href="#" onclick="editNode(= $id ?>, '= htmlspecialchars($tree->getValue($id), ENT_QUOTES) ?>')">編輯</a>
</li>
<?php endforeach; ?>
</ul>
<!-- 編輯用隱藏表單 -->
<form id="editForm" method="post" style="display:none;">
<input type="hidden" name="op" value="mod">
<input type="hidden" name="cid" id="editCid">
新名稱:<input type="text" name="cname" id="editName" required>
<button type="submit">更新</button>
</form>
<script>
function editNode(cid, cname) {
document.getElementById('editCid').value = cid;
document.getElementById('editName').value = cname;
document.getElementById('editForm').style.display = 'block';
document.getElementById('editName').focus();
}
</script>
</body>
</html>
is_deleted 欄位,避免誤刪導致資料遺失。sort_order 讓同層級分類可自訂顯示順序。本文提供了一套完整、可直接部署的 PHP + MySQL 無限分類解決方案,涵蓋資料庫設計、核心類別實作、前台管理介面與進階優化方向。開發者可根據專案需求,直接整合或擴充功能(如多語系、權限控制、API 輸出等)。