首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 遞歸函數返回值陷阱:ECShop 無限分類實戰案例分析

     編輯:PHP 相關知識

問題現場:遞歸「黑洞」吞噬返回值

在 ECShop 二次開發中,需根據分類 ID 遞歸查找頂級分類 ID。初版代碼邏輯正確,但永遠返回 null:

<?php
// ❌ 錯誤版本:遞歸調用未返回值
function getCatTopId($cat_id) {
    if ($cat_id) {
        $sql = "SELECT cat_id, parent_id FROM ecs_category WHERE cat_id = $cat_id AND is_show = 1";
        $res = $GLOBALS['db']->getAll($sql);
        
        if ($res[0]['parent_id'] > 0) {
            getCatTopId($res[0]['parent_id']); // 遞歸調用,但沒有 return!
        } else {
            return $res[0]['cat_id']; // 只有最底層有 return
        }
    } else {
        return 1;
    }
    // 函數隱式返回 null
}

根因分析:調用棧與返回值傳遞

調用棧演示 (cat_id=5, 父鏈: 5→3→1→0):

getCatTopId(5)
  ├─ 查詢 parent_id=3
  ├─ 調用 getCatTopId(3)  ← 這裡缺少 return
  │   ├─ 查詢 parent_id=1
  │   ├─ 調用 getCatTopId(1)  ← 缺少 return
  │   │   ├─ 查詢 parent_id=0
  │   │   ├─ return 1 (頂級 ID)  ← 返回給 getCatTopId(1) 的調用處
  │   │   └─ getCatTopId(1) 結束,隱式返回 null  ← 丟失了 1!
  │   └─ getCatTopId(3) 結束,隱式返回 null  ← 繼續丟失
  └─ getCatTopId(5) 結束,返回 null  ← 最終結果

修正版本:層層傳遞返回值

<?php
// ✅ 正確版本:每層遞歸都要 return
function getCatTopId($cat_id) {
    if (!$cat_id) return 1; // 預設頂級
    
    $sql = "SELECT cat_id, parent_id FROM ecs_category WHERE cat_id = ? AND is_show = 1";
    $res = $GLOBALS['db']->getAll($sql, [$cat_id]); // 使用預處理防注入
    
    if (empty($res)) return 1; // 不存在則默認
    
    $row = $res[0];
    if ($row['parent_id'] > 0) {
        // 關鍵:必須 return 遞歸調用的結果
        return getCatTopId($row['parent_id']);
    } else {
        return $row['cat_id']; // 已是頂級
    }
}

遞歸返回值通用法則

鐵律: 只要函數有返回值,且內部調用了遞歸(或任何子函數)並依賴其結果,**必須在調用處加 return**。

return recursiveCall(...); — 這是將子調用的返回值「接力」傳遞給上一層調用者。

進階優化:遞歸轉迭代 (消除棧溢出風險)

分類層級較深時 (超過 100-200 層),PHP 遞歸會報錯 Maximum function nesting level reached。改用循環:

<?php
function getCatTopIdIterative($cat_id) {
    if (!$cat_id) return 1;
    
    $currentId = $cat_id;
    while (true) {
        $sql = "SELECT cat_id, parent_id FROM ecs_category WHERE cat_id = ? AND is_show = 1";
        $row = $GLOBALS['db']->getRow($sql, [$currentId]); // 只取一行
        
        if (empty($row) || $row['parent_id'] == 0) {
            return $row['cat_id'] ?? 1;
        }
        $currentId = $row['parent_id'];
    }
}

進階優化:路徑枚舉 / 閉包表 (查詢 O(1))

避免遞歸查詢,在數據庫設計層面解決:

方案 A:Path Enumeration (路徑枚舉)


ALTER TABLE ecs_category ADD COLUMN path VARCHAR(255) DEFAULT '' COMMENT '路徑: 0/1/3/5/';

-- 查詢頂級分類:取 path 第二個節點
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(path, '/', 2), '/', -1) AS top_id 
FROM ecs_category WHERE cat_id = 5;
-- 結果: 1

方案 B:Closure Table (閉包表) - 推薦


CREATE TABLE category_ancestors (
    ancestor_id INT UNSIGNED NOT NULL,
    descendant_id INT UNSIGNED NOT NULL,
    depth INT UNSIGNED NOT NULL, -- 0=自身, 1=父級, 2=祖父級...
    PRIMARY KEY (ancestor_id, descendant_id),
    INDEX idx_descendant (descendant_id)
);

-- 查詢頂級分類 (depth 最大者,排除自身 depth=0)
SELECT ancestor_id FROM category_ancestors 
WHERE descendant_id = 5 AND depth > 0 
ORDER BY depth DESC LIMIT 1;

ECShop 具體實戰修改建議

  1. 修復現有函數: 在 includes/lib_goods.php 或對應 lib 文件中找到 get_cat_top_id (或類似名稱),添加缺失的 return。
  2. 添加緩存: 分類結構變動極少,查詢結果應緩存到 Redis/文件,避免重複遞歸查詢。
    
    function getCatTopIdCached($cat_id) {
        $cacheKey = "cat_top_id_{$cat_id}";
        if (($topId = $redis->get($cacheKey)) !== false) return (int)$topId;
        
        $topId = getCatTopIdIterative($cat_id); // 使用迭代版
        $redis->setex($cacheKey, 86400, $topId); // 緩存 1 天
        return $topId;
    }
    
  3. 數據庫遷移: 長期規劃遷移至 Closure Table,徹底解決遞歸查詢性能問題。

總結

  • 遞歸函數必須在遞歸調用處加 return 才能正確傳遞結果。
  • PHP 遞歸深度受限 (xdebug.max_nesting_level 默認 256),深層級結構優先用迭代。
  • 高頻查詢場景,數據庫設計優於代碼優化 (Path/Closure Table + 緩存)。
  • ECShop 等老項目維護時,注意 SQL 注入風險,統一改用預處理語句。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved