會員編輯頁只改了姓名,email維持原值,送出後卻顯示email已被使用。先查唯一驗證是否把正在編輯的那筆會員也算進去。新增資料需要檢查任何一筆是否已使用這個email;更新資料則要排除目前模型,再檢查其他資料。
排除對象必須來自伺服器已取得並確認可修改的模型,不能直接拿表單送來的ID交給ignore。本文用三個合成email比較原值、另一筆已用值與新值,讓更新規則只放過自己,仍擋住真正的重複。
新增與更新的唯一檢查不同
普通unique規則會在指定資料表與欄位查是否已有相同值。如果你正在編輯Ada,資料庫裡本來就有Ada的email;沒有排除自己,維持原email仍會查到一筆,驗證失敗是預期結果。
use Illuminate\Validation\Rule;
$rules = [
'email' => ['required', 'email', Rule::unique('users', 'email')],
];
這組適合不該與任何既有會員重複的新資料,不代表可以直接套到每個更新入口。查修時看的是規則查詢範圍,不是先把原email改成另一個值來繞過錯誤。那樣只掩蓋了更新契約沒排除自身的問題。
用取得的模型排除自己
假設控制器已經取得目前的user,並完成修改權限核對,可以把這個模型交給規則。Laravel會從模型取出對應key,安排排除條件。
$rules = [
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($user),
],
];
這裡的user不是讓使用者任意選擇的排除對象。它應是伺服器根據路由與既有流程載入的資源,接著核對目前使用者是否能更新。排除自身只處理唯一性,不處理資源所有權;兩件事都需要成立。
Laravel官方文件特別提醒,不應把使用者控制的請求輸入交給ignore。不要寫成 ignore($request->input('id')),也不要為了讓例子短就略過可信模型的來源。讓請求參數控制要排除哪筆資料,會破壞你原本想確認的範圍。
如果模型用不同的主鍵名稱,模型傳入方式也能讓規則取得模型key的資訊。手動指定排除值與欄位時,必須核對表結構,不能把會員ID、另一個資料表ID與email欄位名稱混在一起。最小修改通常是沿用已經取得的模型。
三個email,要有三種合理結果
在自有測試資料庫建立兩筆會員:Ada使用 [email protected],另一筆使用 [email protected]。現在編輯Ada,分別測她的原值、另一筆的值,以及不存在的 [email protected]。這些都是合成地址,不寄出郵件。
$candidates = [
'[email protected]',
'[email protected]',
'[email protected]',
];
foreach ($candidates as $email) {
$v = Validator::make(['email' => $email], [
'email' => ['required', 'email',
Rule::unique('users', 'email')->ignore($user)],
]);
dump($email, $v->passes());
}
這段需要在測試專案匯入Validator,並讓user指向Ada那筆模型。不要在正式會員表隨意新增測試資料。若已有專用測試資料,確認它們的ID與email,再跑規則即可;目的只是看排除範圍,不需要觸發完整註冊流程。
本文隔離案例使用PHP8.4.2、Laravel13.34.0與記憶體SQLite。未ignore時,原值與另一筆都失敗,新值通過;ignore可信Ada模型後,原值通過、另一筆仍失敗、新值通過。這三個結果一起看,才能知道不是把unique整個關掉了。
只測原值成功還不夠。若程式誤把別人的模型傳入ignore,可能放過另一筆的email,卻仍拒絕自己。因此保存「自己、別人、新值」三份案例,讓排除邏輯與對象都能被核對。
不要用驗證取代資料庫唯一條件
表單驗證提供較早的錯誤提示,但兩個請求可能同時驗證同一個尚未使用的email。驗證時都查不到,不代表稍後兩次寫入都能安全成立。資料庫的唯一索引仍要依業務要求存在,寫入時也要處理constraint失敗。
本文只比較驗證規則,不執行真正的會員更新,也不模擬併發。規則修好後,測試站還要確認欄位保存、資料庫約束與錯誤回應符合需求。不要把一次Validator通過寫成更新或寄信已成功。
同樣地,如果系統允許在特定租戶內唯一,或只檢查某種狀態的資料,需要在規則與資料庫約束中維持一致範圍。不要隨意加where讓紅字消失,卻讓另一個入口或資料庫的規則與它不同。本文例子採全表email唯一,沒有替你設計租戶或軟刪除策略。
格式與大小寫仍依實際資料規則
unique查的是指定欄位。email是否先整理大小寫、去空白,資料庫的比較與排序規則是什麼,要看既有系統。本文合成值沒有大小寫差異,不把SQLite結果推成所有MySQL設定都用同一種比較方式。
若前端顯示的是整理後email,後端驗證與保存卻使用另一份輸入,也可能在原值更新時出現不一致。把進入驗證與保存的資料流對上,但避免日誌保存完整個人資料;測試可用合成值重現,不需要輸出真會員email。
編輯頁的取消與保存也要分清。用表單送來的舊ID或隱藏欄位,不能代替伺服器取得目前模型。先核實資源,再驗證新值,最後才保存;每一步都保留自己的責任。
回到更新入口確認最小變更
找到更新規則後,用可信user加入ignore,不修改新增入口的查重要求。接著測三種email,再確認未授權會員不能用相同入口修改這筆資料。權限失敗與重複值失敗是不同結果,錯誤畫面也應讓使用者理解目前卡在哪一項。
若原值仍然失敗,先看user是否正確、規則指的表與欄位是否正確,再查是否另有其他紀錄已使用相同email。不要擴大排除範圍去遮住真重複。排除「這一筆」的目的是允許它維持原值,不是允許它與任意其他資料重複。
在Form Request裡沿用同一個資源
如果專案把規則放在Form Request,先確認它拿到的路由資源是否已解析成模型。控制器參數裡看見user,不代表你在規則函式中任意讀出的路由值一定就是同一個模型;路由綁定、參數名稱與授權流程要一致。不要在沒有確認型別的情況下,把一個外部傳來的字串誤當可信資源。
例如更新路由名叫account,規則卻去讀user,可能拿到空值或另一個參數。這時直接加ignore也不能修好。先在合成測試中核對目前模型key,再測原值更新,讓資源解析與唯一規則共同對上。正式站的診斷紀錄只需保存必要的資源識別,不必輸出完整會員內容。
如果新增與更新共用同一份規則函式,可明確以是否有已取得的模型決定排除,而不是只看請求裡是否有id欄位。這樣新增資料仍查全部,更新則只排除伺服器確認的那筆,規則的差異也容易讓下一位維護者讀懂。

評論0