表單送來一個欄位,值剛好是null;後端卻回報「沒有這個欄位」。遇到這種情況,先看程式是不是用了isset()。它同時要求變數存在、值不是null,不能單靠結果判斷陣列裡有沒有這個鍵。要區分「沒傳」與「明確傳null」,應先用array_key_exists()判斷鍵,再處理值。
這個差別常出現在設定更新、資料匯入及API部分更新。例如使用者要清除副標題,系統把null當成清空指令;若後端把null視為未提供,就會保留舊副標題。本文用普通PHP陣列拆開這兩種情況,讓判斷對應到你真正要處理的動作。
先把「有鍵」與「有值」分開看
假設收到的資料是['subtitle' => null]。subtitle這個鍵確實在陣列裡,但它沒有非null的值。array_key_exists('subtitle', $input)會得到true,isset($input['subtitle'])則得到false。兩個結果都符合PHP的規則,只是它們回答不同的問題。
如果資料是空陣列,兩種檢查都得到false。也就是說,只看isset的false,無法知道欄位缺席,還是欄位存在但值為null。這不是資料自動被刪掉,也不是PHP沒有讀到請求;是你的檢查方式把兩種狀態合在一起。
還要留意空字串、數字0及布林false。這些值都不是null,所以isset會得到true。把isset當成「有填內容」也容易出錯:空字串可以通過isset,卻未必符合你的表單規則。鍵是否存在、值是否可接受,應分成兩個判斷。
用五組資料確認結果
建立一個獨立的case.php,貼上以下程式。它只檢查記憶體裡的陣列,不連接資料庫,也不會修改網站設定。執行前確認你使用的PHP命令指向要測試的版本。
<?php
$cases = [
'missing' => [],
'null' => ['subtitle' => null],
'empty' => ['subtitle' => ''],
'zero' => ['subtitle' => 0],
'false' => ['subtitle' => false],
];
foreach ($cases as $name => $input) {
printf("%s: exists=%s isset=%s\n", $name,
array_key_exists('subtitle', $input) ? 'true' : 'false',
isset($input['subtitle']) ? 'true' : 'false');
}
在PHP8.4.2執行php case.php,輸出如下。最值得留意的是第二行:鍵存在,但isset仍然為false。後三行則說明不能把空字串、0、false和null混成同一種狀態。
missing: exists=false isset=false
null: exists=true isset=false
empty: exists=true isset=true
zero: exists=true isset=true
false: exists=true isset=true
如果你的結果不同,先看測試資料是否真的保留null。有些表單流程會把空白欄位轉成null,也有資料處理層會先移除值為null的鍵。檢查的位置很重要:應在你要判斷的那個資料結構上測,而不是只看更早收到的原始字串。
部分更新時,先決定null代表什麼
以下例子約定:未提供subtitle就保留舊值;提供null就清空;提供字串就替換。這是示範用的資料規則,你的API也可以禁止null,但要先寫清楚,前端和後端才不會各自猜測。
$stored = ['subtitle' => '原副標題'];
$patch = ['subtitle' => null];
if (array_key_exists('subtitle', $patch)) {
$value = $patch['subtitle'];
if ($value !== null && !is_string($value)) {
throw new InvalidArgumentException('副標題需為字串或null');
}
$stored['subtitle'] = $value;
}
這段程式先看鍵,所以不會漏掉清空指令;接著只接受字串或null,不讓布林false和數字0悄悄寫入。若你把第一個條件換成isset,null就無法走進更新區塊,原副標題會留在那裡。
這也不是要求所有isset都改成array_key_exists。當你只需要一個存在且非null的值,isset仍然合適。例如某個選填顯示文字為null時就應使用預設值,便不必保留「有傳null」這個差別。重要的是更新契約是否需要區分這兩種輸入。
問號問號與empty不能補回這個差別
$input['subtitle'] ?? '預設文字'很適合取得預設值,但不存在和null都會走到預設文字。若null表示清空,使用這個寫法反而可能把清空指令換成預設內容。先處理鍵存在的分支,再讀值,才保得住原本的意圖。
empty()檢查的範圍又更廣,空字串、0及false等值也可能被視為空。因此會員設定裡的「關閉」false、商品數量0、字串"0",都不宜直接用empty推論成「沒有提交」。如果欄位允許這些值,就應另做型別與範圍檢查,不要以方便的空值判斷取代資料規格。
巢狀資料要逐層確認
array_key_exists只檢查傳入陣列的第一層。資料放在$input['profile']['subtitle']時,不能對最外層查subtitle就認為它不存在。先確認profile是陣列,再檢查內層鍵,避免把null、字串或缺席的父層當成陣列使用。
if (isset($input['profile']) && is_array($input['profile'])
&& array_key_exists('subtitle', $input['profile'])) {
$subtitle = $input['profile']['subtitle'];
}
這裡父層profile為null時會跳過,因為例子只允許profile是一個陣列。若你的規則另外允許profile=null代表清空整個設定,就應增加獨立分支,不能直接套用這段。array_key_exists的第二個參數也必須是陣列;物件屬性需要使用適合物件的檢查方式,不能照搬陣列用法。
表單欄位缺席,也可能是前端行為
假如你處理的是HTML表單,還要先確認欄位是否真的被提交。畫面看得到欄位,不代表請求裡一定包含它;例如前端程式可能在送出前重新整理欄位,或使用JSON序列化只挑選有改過的項目。收到的陣列沒有subtitle時,array_key_exists會正確回傳false,但原因需要回到提交流程找。
檢查時可以使用瀏覽器的網路面板,對照請求送出的欄位名稱與後端接收結果。只看頁面上的輸入框內容,無法證明後端收到哪個鍵。測試資料應用虛構副標題,不要把真實會員內容貼到錯誤畫面或公開討論區。若中間有驗證器、清理器或框架請求物件,也要看它們是否改寫了空字串與null。
將責任分清後,修法才會合適:根本沒有提交鍵,要修前端提交或欄位約定;鍵有提交而值為null,要修後端判斷;鍵和判斷都正確,但畫面沒有清空,則要往寫入及顯示流程查。不要在第一個false就加上預設值,讓問題暫時看起來消失。
用小函式保存更新約定
同一個欄位若在多個入口更新,可以把「缺席保留、null清空、字串替換」整理成一個小函式,再讓每個入口使用相同規則。函式應明確接收陣列與原值,不要依賴不清楚的全域請求;遇到不支援的型別,也應回報輸入問題,而不是先轉成字串。
這樣日後新增匯入工具或管理頁面時,較不容易發生一個入口能清空、另一個卻把null忽略的情況。測試也能直接呼叫函式,確認三種輸入與拒絕型別。只為這個單一欄位建立合適的規則即可,不必為所有空值設計一套龐大的通用轉換,避免把不同功能的null含義混在一起。
改完後,保留三種更新測試
修正後不要只測一個有文字的欄位。至少分別送出空陣列、subtitle=null、subtitle="新文字",確認保留、清空與替換各自符合約定。再加入空字串、0和false,檢查哪些值應被接受、哪些應回報輸入錯誤。
讀取資料與寫入資料也要分開核對:前端可能送了null,後端判斷正確,卻在寫入前又套上預設值。沿著同一個欄位看接收、驗證、更新到最後顯示的結果,比只在某一行換函式更容易找到真正卡住的位置。保留合成測試資料即可,不需要在正式用戶資料上試清空。

評論0