iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Modern Web

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

Day 26:報表模組從零開始,從需求到 Resource 骨架

  • 分享至 

  • xImage
  •  

Day 25:上線前檢查清單,環境變數、快取與佇列 結尾留下一句話,報表模組從零開始,從需求到 Resource 骨架,是接下來要處理的內容。今天要處理的正是這件事,也是第八階段「實戰整合收尾」的第一天。

五個模組裡,最後一個從沒被打開過的抽屜

這套後台走到昨天,功能、關聯、驗證、權限、儀表板都已經齊備,效能調校過,測試也寫了,部署前的環境設定也收斂成一份檢查清單。但回頭看 Day 01:為什麼是 Filament,跟手刻後台與其他套件比起來差在哪 定案的示範情境,商品、訂單、客戶、庫存、報表五個模組裡,前四個模組這二十五天來已經被反覆處理過無數次,商品從一個陽春的 Resource 一路加上表單、驗證、關聯、匯入匯出,訂單串起客戶跟訂單明細,客戶則貫穿整個系列的關聯與篩選練習。唯獨報表模組,從系列第一天定案至今,這個抽屜從頭到尾都還沒有真正被打開過。

今天要說清楚一件事,接下來要做的不是去學一個系列前面沒出現過的新技巧。前二十五天走過的每一個階段,累積下來的其實是一套建置流程,拿到一個新模組的需求,該先問什麼問題、該先確認什麼資料、該套用哪些既有機制,這套流程理應已經內化成一種習慣,而不只是二十五篇各自獨立的操作紀錄。報表模組今天要驗證的,正是這套內化下來的流程,面對一個從沒處理過的全新模組時,是不是真的能直接派上用場,而不需要每次遇到新東西都重新摸索一遍做法。

蓋房子蓋到第五間,前面四間累積下來的施工流程圖,這一間還用不用得上,今天就是要現場給出答案。範圍先講清楚,今天只做兩件事,先釐清報表模組實際該回答管理者哪些業務問題,再依循系列已經建立的流程,建立起報表 Resource 的骨架。圖表呈現跟篩選條件這些比較進階的東西,留給接下來兩天。

動手之前先問問題,管理者想從報表看到什麼

報表模組跟商品、訂單這類具體業務實體有一個明顯的性質差異,值得先攤開來講。商品有自己的資料表,一筆商品資料建立之後就實際存在,訂單也是如此。報表通常不是這樣,它多半不對應一張獨立儲存的資料表,而是把既有的商品、訂單、訂單明細資料彙整起來,重新呈現成管理者想看的樣子。這代表動手寫程式之前,第一步該做的不是急著決定要建立哪些欄位,而是先把管理者實際想從報表裡看到什麼問題的答案,具體列出來。

回到寵物用品批發商這個示範情境,Day 01:為什麼是 Filament,跟手刻後台與其他套件比起來差在哪 定案的業務型態是商品品項多、客戶下單頻率高、需要掌握庫存水位,順著這個業務型態往下推,管理者真正關心的問題並不難列。第一個問題,某段期間的訂單總數跟總金額各是多少,這是最基本的營運體檢,這個月跟上個月比起來訂單量有沒有成長,是管理者每天打開後台第一眼就想確認的數字。第二個問題,哪些商品賣得最好,批發商的商品品項一多,光靠肉眼掃過商品列表根本看不出銷售排名,需要把訂單明細依商品分組加總數量,才能回答這個問題。第三個問題,哪些商品庫存偏低需要留意,庫存數量偏低又持續有訂單在消耗,代表這項商品隨時可能斷貨,這是採購該優先處理的警訊。

這三個問題各自需要彙整不同的資料來源,訂單總數與總金額只需要掃過訂單本身,商品銷售排名需要把訂單明細跟商品資料串起來分組計算,庫存偏低則單純檢視商品的庫存欄位。三個問題攤開來看,沒有一個是靠新增一張報表專用的資料表就能解決的,全部都得從既有資料裡彙整出答案,這正是報表模組跟前面四個模組最根本的不同之處。

這個先問問題再動手的習慣,其實不是報表模組才需要的新規矩。回頭看系列從 Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面 開始建立商品 Resource 之前,同樣得先確認商品這個 Model 跟資料表存不存在,同樣得先想清楚一套商品管理介面至少需要哪些欄位,才會動手下指令生成骨架。只是商品這類具體業務實體的需求相對直覺,一眼就能看出該有名稱、價格、庫存這些欄位,先問問題這個步驟因此容易被忽略。報表模組因為不對應具體業務實體,答案不會自動浮現,這個原本就該做的步驟,重要性在這裡被放大到無法跳過。

動手做骨架,套用系列已經走過的流程

三個業務問題列出來之後,正式進入今天的主戲,建立報表 Resource 的骨架。先講清楚今天要做的資料模型,不是新增一張 reports 資料表,而是建立一個不對應實際資料表的 Eloquent Model,內部方法負責把商品、訂單、訂單明細彙整成一組統計資料。

namespace App\Models;

use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use Illuminate\Support\Collection;

class SalesReport
{
    public static function summary(): array
    {
        return [
            'order_count' => Order::whereBetween('order_date', [now()->startOfMonth(), now()])->count(),
            'total_amount' => Order::whereBetween('order_date', [now()->startOfMonth(), now()])->sum('total_amount'),
        ];
    }

    public static function topSellingProducts(int $limit = 10): Collection
    {
        return OrderItem::query()
            ->selectRaw('product_id, SUM(quantity) as total_quantity')
            ->groupBy('product_id')
            ->orderByDesc('total_quantity')
            ->with('product')
            ->limit($limit)
            ->get();
    }

    public static function lowStockProducts(int $threshold = 10): Collection
    {
        return Product::where('stock', '<=', $threshold)->get();
    }
}

summary、topSellingProducts、lowStockProducts 這三個方法,剛好分別對應剛才列出的三個業務問題。topSellingProducts 用到的分組加總查詢跟預先載入商品關聯,正是 Day 12:把商品、訂單、客戶串起來,第一次像個真正的系統 已經打通的商品、訂單、訂單明細關聯,沒有這層關聯打底,這裡的查詢連寫都無從寫起。這一步是今天骨架建立過程裡明確需要因應報表特性調整的地方,前四個模組的 Model 都直接對應一張真實資料表,報表這個 Model 本質上是一層查詢邏輯的封裝,繼承 Model 反而不合適,改用一個純粹的類別搭配靜態方法,更貼近它實際在做的事。

Model 這層處理完,輪到 Resource 本身,這一步幾乎是直接複製 Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面 走過的流程,先確認資料來源存在,再用指令生成骨架:

php artisan make:filament-resource SalesReport --view

指令生成的目錄結構、四個預設頁面的概念,跟商品 Resource 當時完全一樣。不同的地方在於,報表 Resource 用不到建立跟編輯這兩個頁面,管理者不會手動新增或修改一筆報表資料,這裡只保留列表頁,把建立跟編輯頁面從 getPages() 裡拿掉即可,這是因應報表模組性質做的小幅調整,指令本身生成的骨架不需要動。

列表頁的表格欄位定義,直接沿用 Day 05:表單版面配置,用 Schema 排出一份不擁擠的表單 定案的 Schema 機制,只是套用在表格這一側:

use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use App\Models\SalesReport;

public static function table(Table $table): Table
{
    return $table
        ->query(fn () => SalesReport::topSellingProducts())
        ->columns([
            TextColumn::make('product.name')
                ->label('商品名稱'),
            TextColumn::make('total_quantity')
                ->label('累計銷售數量')
                ->sortable(),
        ]);
}

TextColumn::make 這套語法,跟商品列表頁當初定義欄位用的是同一套 Schema 機制,->label() 設定顯示名稱、->sortable() 開啟排序,這些寫法今天沒有任何一處是重新學的。真正需要留意的調整,是 ->query() 這裡傳入的不是一個普通的 Eloquent 查詢建構器,而是剛才在 SalesReport 裡封裝好的彙整查詢結果。商品、客戶這類 Resource 的表格預設直接讀取對應的資料表,報表 Resource 的表格讀的是一段彙整運算後的結果集,這是資料來源性質不同造成的必然調整,不是 Schema 這套定義方式本身有什麼不同。

報表 Resource 骨架建立流程,三個業務問題對應 SalesReport 三個彙整方法、Resource 生成指令、表格欄位定義,並標示沿用與調整之處

把今天走過的這幾步攤開來看,make:filament-resource 這道指令、TextColumn 這套欄位定義語法,全部是直接複製系列前面已經走過的路,幾乎沒有多想就能完成。真正需要停下來調整的地方,只有兩處,資料來源從一張真實資料表換成彙整查詢的結果,以及表格只留列表頁、拿掉用不到的建立編輯頁。大部分流程可以複製,少部分需要因應模組特性調整,這是今天最務實的結論,既不是整套流程完全一模一樣照抄就能用,也不是報表模組真的需要另外發明一套全新做法。

骨架有了,但這還不是管理者要的報表

回顧今天做完的事,先把管理者實際關心的三個業務問題具體列出來,訂單總數與總金額、哪些商品賣得最好、哪些商品庫存偏低,再依循系列已經建立的流程,建立起報表 Resource 的骨架,能呈現彙整過的基本統計欄位。這個過程證明了一件事,系列前面二十五天累積下來的建置流程,確實可以套用到一個全新模組上,不需要每次遇到新模組都重新發明一套做法,蓋房子蓋到第五間,前四間的施工流程圖今天證明依然用得上。

但今天做出來的骨架也要老實承認侷限。目前的呈現方式還停留在表格欄位的形式,一列一列的商品名稱跟累計銷售數量,管理者真正想看到的報表,往往需要圖表呈現一段時間內的趨勢變化,也需要能依時間區間或商品分類這類條件篩選出想看的範圍,這些今天都還沒有處理。表格骨架只是把彙整後的原始數字攤開來擺著,離一份真正好用的報表還有一段距離。

報表模組加上圖表與篩選條件,是接下來要處理的內容。


上一篇
Day 25:上線前檢查清單,環境變數、快取與佇列
下一篇
Day 27:報表模組加上圖表與篩選條件
系列文
Laravel Filament 從入門到實戰 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言