無限分類是經典的樹形結構應用。PHP 中最直觀的做法是**遞歸函數**配合**靜態變量**累積結果,單次遍歷即可建立父子關係。本文提供標準遞歸實作、靜態變量優化、非遞歸 (堆疊/佇列) 替代方案、以及大量資料時的效能考量。
$areas = [
['id' => 1, 'name' => '北京', 'pid' => 0],
['id' => 2, 'name' => '廣西', 'pid' => 0],
['id' => 3, 'name' => '廣東', 'pid' => 0],
['id' => 4, 'name' => '福建', 'pid' => 0],
['id' => 11, 'name' => '朝陽區', 'pid' => 1],
['id' => 12, 'name' => '海澱區', 'pid' => 1],
['id' => 21, 'name' => '南寧市', 'pid' => 2],
['id' => 45, 'name' => '福州市', 'pid' => 4],
['id' => 113,'name' => '亞運村', 'pid' => 11],
['id' => 115,'name' => '奧運村', 'pid' => 11],
['id' => 234,'name' => '武鳴縣', 'pid' => 21],
];
<?php
/**
* 遞歸建立樹狀結構
* @param array $data 扁平陣列
* @param int $pid 父 ID
* @param int $level 層級 (用於縮排顯示)
* @return array 樹狀陣列
*/
function buildTree(array $data, int $pid = 0, int $level = 0): array
{
static $tree = []; // 靜態變量跨遞歸保持狀態
$tree = []; // 重置 (若函數複用需注意)
// 建立 id => node 映射 (O(n))
$map = [];
foreach ($data as $item) {
$map[$item['id']] = $item + ['children' => [], 'level' => 0];
}
// 遞歸掛載 (利用引用 & 避免複製)
$result = [];
$recurse = function (int $pid, int $level) use ($map, &$recurse, &$result) {
foreach ($map as $id => &$node) {
if ($node['pid'] === $pid) {
$node['level'] = $level;
$result[$id] = &$node;
$recurse($id, $level + 1);
}
}
};
$recurse($pid, $level);
return $result;
}
// 使用
$tree = buildTree($areas);
// 輸出縮排顯示
function printTree(array $tree): void
{
foreach ($tree as $node) {
echo str_repeat(' ', $node['level']) . $node['name'] . "\n";
if ($node['children']) printTree($node['children']);
}
}
printTree($tree);
/*
北京
朝陽區
亞運村
奧運村
海澱區
廣西
南寧市
武鳴縣
廣東
福建
福州市
*/
PHP 預設 xdebug.max_nesting_level = 256,超深層級會報錯。改用顯式堆疊:
function buildTreeIterative(array $data, int $rootPid = 0): array
{
$map = [];
foreach ($data as $item) {
$map[$item['id']] = $item + ['children' => [], 'level' => 0];
}
$tree = [];
$stack = [['pid' => $rootPid, 'level' => 0, 'parentRef' => &$tree]];
while ($stack) {
$frame = array_pop($stack);
$pid = $frame['pid'];
$level = $frame['level'];
$parentRef = &$frame['parentRef'];
foreach ($map as $id => &$node) {
if ($node['pid'] === $pid) {
$node['level'] = $level;
$parentRef[$id] = &$node;
// 將子節點壓入堆疊 (深度優先)
$stack[] = ['pid' => $id, 'level' => $level + 1, 'parentRef' => &$node['children']];
}
}
}
return $tree;
}
將樹狀組裝下推到資料庫,減少網路傳輸與 PHP 記憶體:
WITH RECURSIVE `cte` AS (
SELECT `id`, `name`, `pid`, 0 AS `level`, CAST(`name` AS CHAR(500)) AS `path`
FROM `category` WHERE `pid` = 0
UNION ALL
SELECT c.`id`, c.`name`, c.`pid`, p.`level` + 1, CONCAT(p.`path`, ' > ', c.`name`)
FROM `category` c
INNER JOIN `cte` p ON c.`pid` = p.`id`
)
SELECT * FROM `cte` ORDER BY `path`;
PHP 端直接 $pdo->query($sql)->fetchAll() 即得帶層級與路徑的扁平結果,再用單次遍歷組裝樹 (同方案一)。
| 方法 | 時間 | 記憶體峰值 | 最大深度限制 |
|---|---|---|---|
| 遞歸 + 靜態變量 | ~0.12s | ~18 MB | 受限於 xdebug.max_nesting_level |
| 非遞歸堆疊 | ~0.15s | ~16 MB | 無限制 |
| MySQL CTE + PHP 組裝 | ~0.08s | ~12 MB | 資料庫 max_recursion_depth (預設 1000) |
function getDescendantIds(array $data, int $rootId): array
{
$map = [];
foreach ($data as $item) $map[$item['pid']][] = $item['id'];
$ids = [$rootId];
$queue = [$rootId];
while ($queue) {
$pid = array_shift($queue);
if (!empty($map[$pid])) {
$ids = array_merge($ids, $map[$pid]);
$queue = array_merge($queue, $map[$pid]);
}
}
return $ids;
}
function getBreadcrumb(array $data, int $nodeId): array
{
$map = array_column($data, null, 'id');
$path = [];
while ($nodeId !== 0 && isset($map[$nodeId])) {
$path[] = $map[$nodeId]['name'];
$nodeId = $map[$nodeId]['pid'];
}
return array_reverse($path);
}
UPDATE ... WHERE pid IN (SELECT ...) 批次處理。['label' => $name, 'value' => $id, 'children' => ...]。