Claude Code 實戰:用 AI 除錯 Legacy PHP 專案


如果你還不熟悉 CLAUDE.md 的概念,建議先閱讀: 📖 打造 AI 友善的專案文檔:CLAUDE.md 完整指南(上)


🎯 前言

理論很美好,但實戰才是考驗。

這篇文章記錄一個真實的除錯過程:一個在 Legacy PHP 7.4 企業管理系統(AppSystem)上出現的 bug,我是如何用 Claude Code 從「完全不知道哪裡出問題」到「找出根本原因並修復」的。

整個過程大約花了 45 分鐘。如果用傳統方式,我估計至少要 3 小時


🐛 問題報告

某天下午收到一個 bug 回報:

問題:在 ModuleA 的 TypeB-01 表單儲存後,某些欄位的資料消失了。 重現步驟

  1. 以 OrganizationA 的測試帳號登入
  2. 開啟一筆個案的 TypeB-01 表單
  3. 填寫欄位後儲存
  4. 重新開啟,部分欄位變回空白

環境:只在 OrganizationA 的資料庫(demo-org-1)出現,OrganizationB(demo-org-2)正常

這個描述有幾個特別之處:

  1. 只在特定機構出現 — 暗示問題可能與資料庫差異或機構設定有關
  2. 「部分欄位」消失 — 不是全部,代表邏輯上有條件判斷
  3. 儲存後立刻重新開啟就消失 — 排除前端問題,是後端儲存邏輯的問題

❌ 沒有 CLAUDE.md 時的除錯過程

先說說以前(沒有準備好 context 時)怎麼做的:

我:TypeB-01 表單儲存後某些欄位消失,幫我看看

Claude:請提供你的框架和資料庫結構...

我:這是原生 PHP,沒有框架

Claude:可以分享 Controller 的程式碼嗎?

我:我們沒有 MVC,是直接用 PHP 包含(include)的方式

Claude:那 model 層的程式碼呢?

我:我們沒有 model 層...

Claude:那你的 POST 資料是怎麼處理的?

(來回溝通 15 分鐘還沒找到問題)

每次除錯都要重新說明架構,非常消耗時間。


✅ 有了 CLAUDE.md 之後

有了詳細的 CLAUDE.md,我可以直接描述問題,Claude 自己會從文件中理解架構。

第一步:描述問題,提供最小重現脈絡

我:
TypeB-01 表單儲存後,
部分欄位消失。只在 demo-org-1 出現,demo-org-2 正常。

相關檔案應該在 module_a/ 下。
請問可能的原因有哪些?先分析方向,不要急著看程式碼。

注意這裡我用了一個重要技巧:先要分析方向,不要急著看程式碼

這樣 Claude 會先給出排查思路,而不是直接要求我貼大量程式碼。

Claude 的回應

Claude:
根據 CLAUDE.md 的多租戶架構說明,這個問題很可能屬於以下幾個方向:

1. **資料庫結構差異**(最可能)
   - demo-org-1 和 demo-org-2 的資料表結構可能不同
   - 可能 demo-org-1 缺少某些欄位,導致 INSERT 時失敗但沒有回報錯誤

2. **Session 切換問題**
   - 根據你的 CLAUDE.md,Session 切換資料庫是透過 $_SESSION['mcareDBno']
   - 但 mcareDBno 是 demo-org-1 對應的 ID,需確認這個切換是否正確

3. **POST 參數差異**
   - 如果某些欄位是動態產生的(依機構設定),
     可能 demo-org-1 的設定多了某些欄位,
     而儲存程式沒有處理這些額外欄位

建議先執行:
\`\`\`sql
-- 比較兩個機構的資料表結構
SHOW COLUMNS FROM `demo-org-1`.`typeB_records`;
SHOW COLUMNS FROM `demo-org-2`.`typeB_records`;
\`\`\`

你方便先跑這個查詢嗎?

這個分析非常準確。Claude 直接從 CLAUDE.md 的多租戶架構說明中推斷出方向,而不是要我重新解釋架構。

第二步:提供查詢結果

我執行了 SQL,發現:

-- demo-org-1 的 typeB_records 有一個欄位
-- demo-org-2 沒有的:
| extra_assessment_json | text | YES | NULL |

把結果貼給 Claude:

我:找到了,demo-org-1 的資料表多了一個欄位 extra_assessment_json,
demo-org-2 沒有。這個欄位是 text 型別。

請問這怎麼導致「儲存後欄位消失」的問題?

Claude 的深入分析

Claude:
找到了!這是一個很典型的「靜默失敗」問題。

原因分析:
1. 表單前端有輸入 extra_assessment_json 的欄位
2. 儲存時,PHP 程式嘗試 INSERT/UPDATE 這個欄位
3. 在 demo-org-2,因為資料表沒有這個欄位,PHP 預期這是一個錯誤
4. 但根據你 CLAUDE.md 的 MySQL 設定(--sql_mode=""),
   MySQL 使用寬鬆模式,不會因為多餘欄位而報錯
5. 結果就是:整個 UPDATE 語句被 MySQL 忽略了(或只更新了部分欄位)

等等,讓我重新想想...

更可能的情況:
- 因為 MySQL 寬鬆模式,不是 UPDATE 被忽略,而是:
- 儲存程式使用了 INSERT ... ON DUPLICATE KEY UPDATE 或類似語法
- 當 demo-org-1 的 extra_assessment_json 欄位有 NOT NULL constraint,
  但程式沒有提供這個欄位的值時,MySQL 靜默插入了空值
- 下次讀取時,主查詢只取特定欄位,extra_assessment_json 的空值
  觸發了某個條件判斷,導致其他欄位也被過濾掉

請找一下儲存 TypeB-01 的 PHP 程式,看看有沒有這樣的條件:
\`\`\`php
if ($extra_assessment_json) {
    // 只有這個欄位有值才顯示某些資料
}
\`\`\`

第三步:找到程式碼

我根據 CLAUDE.md 中記錄的路由規則(?mod=xxx&func=yyy),找到對應的程式檔案,搜尋相關邏輯:

我:找到了,在 module_a/typeB/view.php 有這段:

<?php
$data = getRecord($id, $_SESSION['mcareDBno']);
if (!empty($data['extra_assessment_json'])) {
    $extra = json_decode($data['extra_assessment_json'], true);
    // 從 extra 中取出欄位覆蓋 $data
    $data = array_merge($data, $extra);
}
// 下面才是顯示邏輯

Claude 找到根本原因

Claude:
找到了!這就是問題所在。

**根本原因**:
`array_merge($data, $extra)` 這行會用 $extra 覆蓋 $data 的值。

在 demo-org-1:
- extra_assessment_json 欄位存在,但儲存時沒有正確填入值(可能是空字串 "")
- `empty("")` 在 PHP 中是 true,所以 `!empty($data['extra_assessment_json'])` 是 false
- 但等等...你說欄位消失,所以問題可能在儲存那邊

讓我換個方向:儲存程式是哪個?
我:儲存在 module_a/typeB/save.php

我提供了 save.php 的關鍵片段,Claude 立刻找到問題:

Claude:
找到了!在 save.php 的最後:

\`\`\`php
// 問題出在這裡
$fields = getTableColumns('typeB_records', $dbno);
$data = array_intersect_key($_POST, array_flip($fields));
// 只儲存資料表中存在的欄位
executeUpdate('typeB_records', $data, $id, $dbno);
\`\`\`

程式的邏輯是「只儲存資料表中存在的欄位」,這本身是對的,
可以防止 SQL injection。

但問題在於:
1. demo-org-1 多了 extra_assessment_json 欄位
2. 前端表單「沒有」這個欄位的輸入(因為這是後來新加的欄位)
3. 所以 $_POST 裡沒有 extra_assessment_json
4. 但 demo-org-1 的資料表有這個欄位,且目前是空值 ""
5. 執行 UPDATE 後,extra_assessment_json 被更新為 null 或空字串

然後在 view.php 讀取時:
- empty(null) = true,所以 array_merge 沒執行,沒問題
- 但如果 extra_assessment_json 之前有資料,就被清空了

**解決方案**:
在 save.php 中,不要更新 extra_assessment_json(除非前端有明確送出這個欄位):

\`\`\`php
$fields = getTableColumns('typeB_records', $dbno);
// 排除不應由這個表單更新的欄位
$excluded = ['extra_assessment_json', 'created_at', 'created_by'];
$fields = array_diff($fields, $excluded);
$data = array_intersect_key($_POST, array_flip($fields));
executeUpdate('typeB_records', $data, $id, $dbno);
\`\`\`

但在修改前,建議先確認 extra_assessment_json 的用途,
避免影響其他使用這個欄位的功能。

🛠️ 實施修復

根據 Claude 的建議,我去查了 extra_assessment_json 的使用情況,確認這個欄位只被另一個功能(進階評估模組)使用,save.php 確實不應該清空它。

修復很簡單:

// 修改前
$fields = getTableColumns('typeB_records', $dbno);
$data = array_intersect_key($_POST, array_flip($fields));

// 修改後
$fields = getTableColumns('typeB_records', $dbno);
// 排除不應由此表單更新的欄位
$readonly_fields = ['extra_assessment_json', 'created_at', 'created_by'];
$fields = array_diff($fields, $readonly_fields);
$data = array_intersect_key($_POST, array_flip($fields));

修復後測試:

  • ✅ demo-org-1:欄位不再消失
  • ✅ demo-org-2:原有功能正常
  • ✅ 進階評估模組:extra_assessment_json 不受影響

📊 Before / After 完整對比

除錯流程對比

步驟沒有 CLAUDE.md有 CLAUDE.md
描述問題需要解釋架構(10 分鐘)直接描述症狀(2 分鐘)
取得排查方向來回溝通才理解(20 分鐘)Claude 直接給出方向(3 分鐘)
找到相關程式需要自己搜尋(30 分鐘)Claude 依路由規則指引(5 分鐘)
分析根本原因逐步引導,多次誤解(40 分鐘)精準定位(10 分鐘)
確認修復方案需要自己驗證(20 分鐘)Claude 提供可直接應用的程式碼(5 分鐘)
總計120 分鐘25 分鐘

溝通品質對比

❌ 沒有 CLAUDE.md
- 反覆解釋「我們沒有 MVC、沒有 Laravel、沒有框架」
- Claude 給出的建議不適用(建議用 Eloquent、建議重構 Session)
- 每次都從零開始建立脈絡

✅ 有 CLAUDE.md
- Claude 自動理解多租戶架構
- 知道 MySQL 使用寬鬆模式(sql_mode="")
- 知道 Session 變數名稱(mcareDBno)
- 知道路由規則(mod/func 參數)
- 給出的建議直接可用

💡 讓 AI 除錯更有效率的技巧

從這個案例中,我整理出幾個實用技巧:

技巧 1:先要分析方向,再看程式碼

❌ 低效的問法
「這段程式碼有什麼問題?[貼上 200 行程式碼]」

✅ 高效的問法
「這個功能出現 X 問題,症狀是 Y,只在 Z 情況發生。
先分析可能的原因方向,不要急著看程式碼。」

讓 Claude 先形成假設,再有針對性地尋找驗證證據,比直接餵入大量程式碼更有效率。

技巧 2:描述「差異」而不是「問題」

❌ 低效
「表單儲存有問題」

✅ 高效
「在 demo-org-1 出現,demo-org-2 正常。
儲存後立刻重開就消失,說明不是前端快取問題。」

差異性資訊能幫助 Claude 快速縮小範圍。

技巧 3:讓 Claude 自己說明下一步

我固定會在問題描述後加上:
「你需要哪些資訊來繼續排查?」

這樣 Claude 會主動告訴我需要提供什麼,
而不是我漫無目的地貼程式碼。

技巧 4:善用 CLAUDE.md 中的已知限制

提問時可以引用 CLAUDE.md 中的設定,幫助 Claude 排除某些可能性:

例:
「根據 CLAUDE.md,MySQL 使用 sql_mode="",
所以不是 SQL 語法錯誤的問題。
請從其他方向排查。」

技巧 5:分次提供資訊,不要一次貼太多

❌ 一次貼 5 個檔案的程式碼,要 Claude 找問題

✅ 先描述症狀 → 讓 Claude 給方向 →
   提供最相關的一個檔案 → 根據 Claude 的分析提供更多資訊

迭代式的資訊提供,比一次給大量資訊更有效率。


🔍 CLAUDE.md 中哪些內容最有幫助?

這次除錯中,CLAUDE.md 裡以下幾個章節起到了關鍵作用:

1. 多租戶架構說明

# 在 CLAUDE.md 中記錄的內容
## 多租戶架構

每個機構使用獨立資料庫(demo-org-{id})
- demo-org-1 對應 OrganizationA
- demo-org-2 對應 OrganizationB
- Session 切換透過 $_SESSION['mcareDBno']

有了這個,Claude 立刻知道「只在某機構出現」代表可能是資料庫結構差異。

2. MySQL 設定說明

## 技術限制

MySQL 使用 sql_mode=""(寬鬆模式)
- 允許 SELECT 非 GROUP BY 欄位
- 多餘欄位不報錯(靜默忽略)

這讓 Claude 推斷出「靜默失敗」的可能性。

3. 路由規則

## 程式架構

路由規則:?mod=xxx&func=yyy
module_a/typeB/view.php ← 對應 ?mod=typeB&func=view
module_a/typeB/save.php ← 對應 ?mod=typeB&func=save

這讓 Claude 能直接指出相關檔案的位置。


🎉 結語

這次除錯讓我更深刻體會到:CLAUDE.md 不只是讓 AI 理解你的專案,更是讓 AI 成為真正的除錯夥伴。

當 AI 理解你的架構、知道你的限制、了解你的系統特性,它就能從症狀直接推斷到根本原因,而不是每次都從零開始。

如果你還沒有準備 CLAUDE.md,建議從今天開始。哪怕只是把最常見的技術限制和路由規則寫下來,除錯效率也會有明顯提升。


📎 相關文章