iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Modern Web

Laravel Filament 從入門到實戰系列 第 15 篇

Day 14:儲存前後可以掛什麼,庫存異動的自動計算

  • 分享至 

  • xImage
  •  

Day 13:表單驗證不只是必填,跨欄位規則怎麼寫 結尾講得很清楚,驗證只發生在使用者按下儲存之前的那一刻,一筆訂單明細通過驗證、真正存進資料庫之後,理應接著發生的另一件事,商品庫存水位該如何跟著變動,驗證規則完全無能為力。這句話往前推,其實是一條從 Day 12:把商品、訂單、客戶串起來,第一次像個真正的系統 就開始懸著的債,那篇明白寫著,訂單明細裡填的數量跟商品自身的庫存數量,是兩個完全獨立的數字,誰變動都不會影響另一邊。

今天我們來討論這個問題。

從業務語意到程式碼,這筆帳今天結清

先把問題講得再具體一點。回頭看 Day 01:為什麼是 Filament,跟手刻後台與其他套件比起來差在哪 定案的示範情境,寵物用品批發商賣出商品,理應反映在庫存水位上,這句業務語意寫在系列第一天,寫到今天為止都還只是一句描述,示範專案裡完全沒有程式碼對應它。

所以,今天要做的事情具體而明確,鎖定這一個真實存在的業務缺口,訂單明細儲存之後,對應商品的庫存數量欄位該如何自動被異動,不打算另外挑一個無關的生命週期事件來練手感。

在動手之前,先擋掉一個很直覺的做法。多數人第一次遇到這種需求,會想直接在訂單明細的表單儲存邏輯裡,順手加一段「存檔的時候,把商品庫存也減一減」的程式碼。

這個做法能動,但擺錯了位置。

表單儲存這個動作,責任應該只有一個,把使用者填的資料存進資料庫。庫存異動是這筆資料確定存進去之後,衍生出來的另一個責任。

兩件事攪在一起,程式碼會變得難以單獨測試,日後訂單明細如果多了命令列工具或 API 這類額外的建立途徑,那段寫死在表單邏輯裡的庫存異動同樣不會被觸發。這正是今天要示範的生命週期掛載點存在的理由,讓庫存異動這件事掛在資料本身的變動上,跟任何一個特定的操作入口脫鉤。

儲存前跟儲存後,兩個時機各自該做的事

Eloquent Model 本身早就有一整套生命週期事件,creating、created、saving、saved、deleting、deleted,這些事件的存在跟用法讀者應該不陌生。這裡要先講清楚一件事,Filament 在建立或更新一筆資料的時候,底層仍然是透過這些既有事件在運作,並沒有另外發明一套平行機制。今天要看的重點,是這些讀者原本就熟悉的 Eloquent 生命週期事件,跟 Filament 這一層之間如何銜接。

事件名稱裡的 -ing 跟 -ed 是一個明確的分界,-ing 結尾的事件在資料真正寫入資料庫之前觸發,-ed 結尾的事件在資料確定寫入之後才觸發。這個命名規則背後對應的,是兩種完全不同性質的邏輯。

儲存前的時機,適合處理「這筆資料在存進去之前,需要被調整或檢查的內容」。如果把這個時機比喻成出貨前的最後檢查,倉庫人員在包裹封箱之前,會再核對一次品項數量對不對、地址填齊了沒有,這些檢查跟調整都發生在包裹離開倉庫之前,一旦包裹封箱寄出,再回頭改內容物就太晚了。表單驗證某種程度上也屬於這個範疇,只是驗證發生的位置更早,在資料進 Model 之前就先擋掉了。

儲存後的時機,適合處理「這筆資料確定存進去之後,需要連動影響到別的地方」。

延續剛才的比喻,包裹真的出貨之後,理應通知下一個部門,貨已經出門了,該去更新庫存清單。這個動作沒辦法在出貨前做,因為出貨前這件事根本還沒發生,也沒必要拖到很久以後才做,包裹一出門就該立刻通知。

庫存異動明確屬於後者。它要影響的是商品這張資料表,是訂單明細之外的另一筆資料,只有在訂單明細確定成功存進資料庫之後,這個連動才有意義去觸發。如果在儲存前就去扣庫存,一旦後續儲存過程中出了什麼差錯導致這筆明細沒有真的存進去,庫存卻已經被扣了,兩邊的數字反而對不起來。今天要掛的位置,是訂單明細儲存後的那個時機點。

訂單明細儲存後,自動異動商品庫存

動手之前先決定程式碼要放在哪裡。同樣一段邏輯,可以直接寫在 Model 的 booted() 方法裡監聽事件,也可以獨立拉成一個 Observer 類別。兩者本質相同,差別只在程式碼怎麼組織,事件邏輯一多,集中放在 Observer 裡會比塞在 Model 內部更好維護,今天用 Observer 示範。

先建立一個對應訂單明細 Model 的 Observer:

php artisan make:observer OrderItemObserver --model=OrderItem

這個指令會在 app/Observers 目錄下產生一個 OrderItemObserver 類別,裡面預先準備好對應各個生命週期事件的方法。把庫存異動的邏輯寫進 saved 這個方法:

namespace App\Observers;

use App\Models\OrderItem;

class OrderItemObserver
{
    public function saved(OrderItem $orderItem): void
    {
        if ($orderItem->wasRecentlyCreated) {
            $orderItem->product()->decrement('stock', $orderItem->quantity);
        }
    }
}

拆解這段程式碼在做的事情。saved 事件在一筆訂單明細建立或更新之後都會觸發,這裡只想處理「新增」這一種情境,所以用 wasRecentlyCreated 這個 Eloquent 內建的屬性判斷,這筆資料是不是剛剛才第一次被建立。確認是新增之後,透過 product() 這個關聯,直接對商品那張資料表的 stock 欄位做 decrement(),扣掉的量就是這筆訂單明細填入的數量。decrement() 是資料庫層級的原子操作,不需要先查出商品、改欄位、再存回去,也不會因為兩筆訂單同時儲存而發生數值互相覆蓋的競爭問題。

接著補上刪除情境。一筆訂單明細如果被刪除,理應把剛才扣掉的庫存數量還原回去,這段邏輯掛在 deleted 這個事件上:

namespace App\Observers;

use App\Models\OrderItem;

class OrderItemObserver
{
    public function saved(OrderItem $orderItem): void
    {
        if ($orderItem->wasRecentlyCreated) {
            $orderItem->product()->decrement('stock', $orderItem->quantity);
        }
    }

    public function deleted(OrderItem $orderItem): void
    {
        $orderItem->product()->increment('stock', $orderItem->quantity);
    }
}

deleted 在這筆訂單明細確定從資料庫消失之後觸發,此時這筆明細的 quantity 欄位值還讀得到,用同一個數字反向對商品的 stock 做 increment(),剛才扣掉多少,這裡就還回多少。

最後把這個 Observer 註冊上去,讓 Laravel 知道 OrderItem 這個 Model 要交給 OrderItemObserver 監聽,在 AppServiceProvider 的 boot() 方法裡呼叫 observe():

use App\Models\OrderItem;
use App\Observers\OrderItemObserver;

public function boot(): void
{
    OrderItem::observe(OrderItemObserver::class);
}

註冊完成之後,這套邏輯掛在 OrderItem 這個 Model 本身,跟 Filament Resource 的表單儲存流程各自獨立。這代表無論這筆訂單明細是透過 Filament 的 Relation Manager 建立,還是透過命令列工具、Job,甚至是未來可能出現的 API,只要走的是 OrderItem 這個 Model,庫存異動都會一致發生,不需要在每一個進入點各自補一次同樣的邏輯。

這裡也老實點出一個目前刻意不展開的情境。如果一筆訂單明細後續被編輯,數量從五改成八,理論上該做的事情,是先把原本扣掉的五還原,再重新扣減八,計算順序一旦顛倒,庫存數字就會對不起來。這件事聽起來只是多一個步驟,實際上牽涉判斷順序跟資料一致性,複雜度明顯高過新增跟刪除。

今天先把新增跟刪除這兩個最基本、也最迫切的情境處理乾淨,編輯情境的重新計算留給讀者依實際專案需求另外擴充。

走一次下單到庫存異動,親眼看數字變化

實作完成,實際操作驗證一次。打開一張既有訂單的編輯頁,商品明細區塊點下新增,選中飼料這個商品,數量填十,儲存。切換到商品 Resource,找到飼料這一筆,庫存數量欄位確實比剛才少了十。這不是憑印象猜測的結果,是打開資料庫或重新整理列表就能直接看到的數字變化。

回到訂單明細,把剛才新增的這筆明細刪除,再回頭檢查飼料的庫存數量,數字確實還原成刪除前的樣子,剛才扣掉的十又補了回來。新增跟刪除這兩種最基本的操作,都照著預期運作。

這一刻值得停下來多說一句。這是全系列第一次,讓兩個不同模組的資料,在儲存的當下自動互相影響,不需要使用者自己手動去改庫存數字。Day 01:為什麼是 Filament,跟手刻後台與其他套件比起來差在哪 定案的那句業務語意,賣出商品理應影響庫存水位,寫在系列第一天,一路懸到今天,這是它第一次真正被程式邏輯實現,不再只是文件裡的一句話。

OrderItemObserver

債清了,但誰都能觸發這個動作

今天處理的落差可以完整回顧一次。從 Day 1 定案的業務語意、Day 12 具體點名、Day 13 結尾再次確認,訂單明細跟商品庫存之間的連動,今天正式打通。訂單明細儲存後,商品庫存會自動異動,刪除時也會自動還原,這套邏輯掛在 OrderItem 這個 Model 本身,跟特定的操作入口脫鉤。

但這裡要誠實點出一個新出現的落差。這套自動異動邏輯,目前對任何登入這套後台的使用者都無條件觸發,只要打得開訂單明細的編輯畫面,新增或刪除一筆明細,庫存就會跟著變動。倉管人員負責處理庫存進出,異動庫存合情合理。但業務人員負責接單,財務人員負責對帳,這兩種角色是不是也該擁有一模一樣的操作範圍,能夠任意新增或刪除訂單明細、連帶觸發庫存異動,目前的後台完全沒有區分,誰都能點開這個畫面,誰都能觸發這整套動作。

接上 Laravel 既有的 Policy,誰能看見這個操作按鈕,是接下來要處理的內容。


上一篇
Day 13:表單驗證不只是必填,跨欄位規則怎麼寫
系列文
Laravel Filament 從入門到實戰 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言