Laravel篩選集合後JSON變物件:用values整理索引

API原本回傳一個商品陣列,後端加了 filter 後,前端突然不能再用同樣的清單操作。回應內容看起來仍有兩件商品,最外層卻從方括號變成大括號,還多了 "0"、"2" 這些key。先查集合篩選後保留下來的索引,不必立刻修改前端把物件硬轉陣列。

Laravel的集合 filter()會保留原key。PHP把不連續的數字key編碼為JSON時,產生的是物件;如果API契約需要一份清單,在篩選後呼叫 values()重新排列索引,就能讓最外層保持陣列形狀。這裡的索引與資料本身的商品ID是不同東西。

刪掉第二項後,索引不是自動連續

用三筆合成資料重現即可。id是業務資料,active是本例的篩選條件,不接資料庫,也不假設你的商品表一定有同樣欄位。

$items = collect([
    ['id' => 11, 'active' => true],
    ['id' => 12, 'active' => false],
    ['id' => 13, 'active' => true],
]);
$filtered = $items->filter(fn ($row) => $row['active']);
dump($filtered->keys()->all());
dump($filtered->toJson());

留下來的是id11與id13,但外層key是0與2。這是因為篩選保留原本位置的key,沒有把2往前改成1。在PHP8.4.2、Laravel13.34.0的案例裡,toJson()輸出是下面這種形狀。

{"0":{"id":11,"active":true},"2":{"id":13,"active":true}}

這份JSON合法,伺服器沒有編碼失敗;只是它是以key索引的物件,而非前端期待的陣列。不要把JSON是否合法與是否符合API契約混在一起。回應200、能解析JSON,都不能代替檢查資料型別與欄位形狀。

用values重排外層,不改商品ID

如果這個回應是供 v-for、表格或一般清單使用,且API約定最外層是一個陣列,可在篩選之後整理索引。

$result = $items
    ->filter(fn ($row) => $row['active'])
    ->values();
dump($result->keys()->all());
dump($result->toJson());

此時外層key變成0與1,JSON為陣列;兩件商品的id仍是11與13。values()整理的是集合索引,不是重新編號資料列,更不會替資料庫更新主鍵。

[{"id":11,"active":true},{"id":13,"active":true}]

同一組例子還檢查了全部被排除的情況:結果再呼叫 values()->toJson()得到 []。前端因此可以用同樣的陣列契約處理零筆、一筆、多筆,不必看到空結果時又改另一種分支。不過真實API若外面還有 data或分頁欄位,應檢查的是約定的清單位置,不是把整份回應都改成陣列。

在Laravel測試專案的Tinker可以直接執行上述三筆資料範例。比對時同時看 keys()、toJson()與 pluck('id'):前兩個確認形狀,最後一個確認業務ID沒變。只看前端畫面能顯示,可能漏掉回傳順序或ID被誤改的問題。

不是所有集合都應該values

如果資料本來就按明確的業務key索引,保留key可能是正確需求。例如回傳以語系代碼為key的設定,或以商品ID快速查找的映射,物件正好符合契約。這時一律 values()反而會丟掉原本有意義的key,讓呼叫端無法直接查找。

因此修正前先問「這是list,還是map」。list著重順序與項目;map著重key到value的對應。兩者都能包含相同資料,但存取方式不同。不要因為前端某個套件喜歡陣列,就在所有API輸出上加values,尤其既有客戶端可能已經依key存取。

如果一份回應既要清單順序,又需要穩定識別,可以把業務ID留在每個項目裡,外層維持清單。像範例的id欄位就做這件事。用陣列位置代替商品ID會讓篩選、排序、插入後的識別變動,並不是整理索引想解決的問題。

巢狀分組也要逐層判斷。外層用分類slug當key,內層才是項目陣列時,只整理需要當清單的內層;在最外層呼叫values,可能把分類key一起丟掉。不要把單層範例推成任意分組資料的通用改法,先看呼叫端需要哪一層是陣列。

原生JSON與API Resource可能有不同處理

本文比較的是普通Collection直接 toJson()的結果。若回應使用Eloquent API Resource collection,Laravel官方文件說明預設會重設集合的數字key,並提供 preserveKeys來保留key。這是資源層的行為,不能直接當成每個普通集合方法都會重排。

維護API時,先確認最後序列化的是普通集合、ResourceCollection、已轉成PHP陣列,還是自己呼叫 json_encode。同樣前面用了filter,最後輸出層不同,觀察到的JSON形狀也可能不同。若資源層本來已整理key,就不必為了本文案例重複改動它。

也不要先把集合 toJson()成字串,再把那個字串交給 response()->json(),期待得到和直接回傳資料相同的形狀。JSON字串與待序列化的資料是兩種輸入;不必要的雙重編碼會讓呼叫端先得到字串,再自行解析一次。選一個明確的輸出層,讓資料直到那裡再完成序列化。

如果要在控制器回傳本例的資料,可以把整理好的集合交給適合的JSON回應方式,並核最終網路回應。Tinker看到的字串只是幫你定位索引問題;正式回應還可能經過資源轉換、包裝或中介層,應以最終資料契約為準。

避免把型別錯誤藏在前端

有些前端修法會對物件使用 Object.values(),讓畫面暫時恢復。若後端本來就承諾陣列,這樣做可能掩蓋不同請求回不同型別的問題。較合理的修正是讓後端保持契約,再在整合測試裡檢查清單位置是否為陣列。

要覆蓋的案例至少包含保留全部、只保留第一與第三筆、全部被排除。第二種特別能發現索引缺口,因為只篩掉最後一筆時,key還可能連續,看不出差異。測試資料不是越多越好,而是要真的產生會改變JSON形狀的條件。

最後核對順序、ID與數量。values()解決的是索引與序列化形狀,不會替你選對商品、授權欄位或排序。前端恢復顯示後,也應確認篩選條件仍符合需求,且沒有為了重排而把未公開項目一起回傳。

如果看到JSON突然由陣列變物件,先印出篩選後的key。只有這份資料確實應當是清單,才在合適的位置加values,並保存空結果與索引有缺口的測試,避免往後改filter時再次出現同樣的型別變動。

在呼叫端也可以直接檢查型別,讓不符合契約的資料明確失敗。例如讀取約定的清單後,用 Array.isArray()檢查,而不是一開始就把所有輸入轉成陣列。這樣能辨別伺服器回傳的是清單、錯誤物件,還是格式已變動的另一種回應。

const response = await fetch('/api/items')
const items = await response.json()
if (!Array.isArray(items)) {
  throw new Error('items response must be an array')
}

這段假定API直接回最外層陣列;若你的契約是 { data: [...] },就應檢查data,而不是照抄最外層判斷。HTTP失敗與業務錯誤也要按自己的API處理,不能只靠一個型別檢查判定資料可用。它的作用是讓索引造成的形狀變動被及早看見,方便維護者回到正確的輸出層修正。

若既有客戶端已把它當映射使用,先協調契約版本,再改輸出,不要直接移除有意義的key。

參考資料

原文鏈接:https://wntheme.com/laravel-collection-values-json-shape/,轉載請註明出處。
0

評論0

顯示驗證碼
沒有帳號?註冊  忘記密碼?