不只會用,更要會『造』!Laravel 自訂驗證與 Middleware 黑魔法,打造無懈可擊的 API 防線

2025/07/16 | Laravel技術分享

不只會用,更要會『造』!Laravel 自訂驗證與 Middleware 黑魔法,打造無懈可擊的 API 防線

哈囉,我是浪花科技的資深工程師 Eric。寫了這麼多年的程式,我看過太多專案因為前期貪快,在驗證(Validation)和中介層(Middleware)這兩道防線上便宜行事,結果後期需求一改,整個 Controller 邏輯變得跟麻花捲一樣,牽一髮動全身,改個小東西就噴出一堆 bug,最後變成誰都不想碰的技術債孤兒。這實在是,唉,工程師的痛啊。

很多人對 Laravel 的驗證可能還停留在 'required''email''min:8' 這些基本功。對於 Middleware 的理解,可能也只是 `auth` 或 `guest` 這種內建的守門員。但說真的,真實世界的業務邏輯哪有這麼單純?「老闆,這個欄位要驗證台灣手機號碼格式」、「Eric,這個 API 只有白金會員而且帳號啟用超過三個月的才能呼叫」、「那個…後台的上傳功能,我們要限制總監等級的人一天只能上傳 10 次」。這些奇奇怪怪、刁鑽的需求,光靠內建規則是遠遠不夠的。今天,我們不談那些基礎,我們要來點硬核的,深入聊聊如何「打造」屬於你自己專案的驗證規則與智慧型 Middleware,從源頭就把程式碼的結構做對,寫出讓三個月後的你和接手的同事都會感謝你的優雅程式碼。

為何內建規則不夠用?客製化驗證的時機點

Laravel 內建的驗證規則庫非常強大,幾乎涵蓋了 80% 的日常開發場景。但剩下的 20%,往往就是專案的核心業務邏輯所在,也是最容易寫出「髒程式碼」的地方。在動手客製化之前,我們先來看看哪些情況下,你應該毫不猶豫地捲起袖子,打造自己的驗證器。

場景一:獨特的業務邏輯

這是最常見的情況。每個國家、每個行業都有其獨特的資料格式。例如:

  • 身份驗證: 台灣的身分證字號、公司的統一編號,它們都有固定的演算法則,這就不是一個簡單的 stringdigits:10 可以解決的。
  • 格式限制: 像是台灣的手機號碼,必須是 09 開頭,後面跟著 8 位數字。
  • 專案特定規則: 例如,會員的暱稱不能包含不雅詞彙,這就需要去比對你的禁用詞資料庫。

把這些邏輯直接寫在 Controller 裡?千萬不要!這會讓你的 Controller 變得臃腫不堪,而且這段邏輯完全無法在其他地方複用。

場景二:需要與外部資料互動的驗證

有時候,驗證一個欄位的有效性,需要查詢資料庫或呼叫第三方 API。例如:

  • 優惠券代碼: 使用者輸入一個優惠券代碼,你需要去 `coupons` 資料表查詢這個代碼是否存在、是否過期、是否已被使用。
  • 會員邀請碼: 驗證使用者填寫的邀請碼是否為一個有效的會員 ID。
  • 庫存檢查: 在購物車結帳時,驗證每項商品的庫存是否足夠。

這種驗證顯然無法用內建規則完成,它天生就是客製化規則的絕佳應用場景。

場景三:更複雜的條件式驗證

雖然 Laravel 提供了 required_ifrequired_with 等條件式驗證,但當邏輯變得更複雜時,它們就顯得捉襟見肘了。例如:「如果 `payment_method` 是『信用卡』,那麼 `credit_card_number` 和 `cvv` 欄位為必填;如果 `payment_method` 是『ATM 轉帳』,則 `bank_code` 欄位為必填。」這種高度依賴其他欄位狀態的驗證,用客製化規則處理會讓邏輯清晰非常多。

Laravel 驗證客製化實戰:從 Rule 物件到 Closure

好了,理論說完了,我們來點實際的。Laravel 提供了兩種主流的客製化驗證方法:Rule 物件和 Closure(閉包),它們各有優劣,適用於不同情境。

方法一:優雅的屠龍刀 — 建立專屬的 Rule 物件

當你的驗證邏輯相對複雜,或者需要在專案中多處重複使用時,建立一個專屬的 Rule 物件是最佳選擇。它符合物件導向的原則,讓你的驗證邏輯獨立、可測試、好維護。

讓我們用「驗證台灣手機號碼」這個需求來實作。首先,透過 Artisan 指令建立一個 Rule 檔案:

php artisan make:rule IsTaiwanMobileNumber

這會在 `app/Rules` 資料夾下產生一個 `IsTaiwanMobileNumber.php` 檔案。我們來修改它:

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class IsTaiwanMobileNumber implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        // 規則:必須是字串,且符合 09 開頭、總長度 10 碼的數字格式
        return is_string($value) && preg_match('/^09\d{8}$/', $value);
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return '請輸入有效的台灣手機號碼格式 (例如: 0912345678)。';
    }
}

看到了嗎?`passes` 方法負責回傳 `true` 或 `false` 來決定驗證是否通過,而 `message` 方法則定義了驗證失敗時的錯誤訊息。就是這麼單純!

使用起來也極其優雅,不論是在 Controller 或是 FormRequest 中:

use App\Rules\IsTaiwanMobileNumber;

$request->validate([
    'mobile' => ['required', new IsTaiwanMobileNumber],
]);

你看,驗證邏輯被完美封裝,Controller 的程式碼乾淨到不行。這就是我所謂的,寫出讓未來自己感謝的程式碼。

方法二:輕巧的瑞士刀 — 使用 Closure 快速驗證

有時候,某些驗證邏輯可能只會在某個特定的地方用上一次,為此特地建立一個 Rule 檔案似乎有點殺雞用牛刀。這時候,Closure(閉包)就派上用場了。

假設我們有一個需求:註冊時,使用者名稱 `username` 不能是 `admin`、`root` 或 `administrator`。這種一次性的簡單需求就很適合用 Closure。

use Illuminate\Support\Facades\Validator;

$validator = Validator::make($request->all(), [
    'username' => [
        'required',
        'string',
        'max:50',
        function ($attribute, $value, $fail) {
            $reservedNames = ['admin', 'root', 'administrator'];
            if (in_array(strtolower($value), $reservedNames)) {
                $fail('此 :attribute 已被系統保留,請更換一個。');
            }
        },
    ],
]);

在驗證規則陣列中直接傳入一個匿名函式。這個函式會接收三個參數:屬性名稱 (`$attribute`)、使用者輸入的值 (`$value`),以及一個失敗時要呼叫的回呼函式 (`$fail`)。如果驗證失敗,就呼叫 `$fail` 並傳入錯誤訊息。是不是非常直覺快速?

Middleware 不只是守門員:打造更智慧的 API 關卡

如果說 Validation 是檢查訪客的「攜帶物品」是否合規,那麼 Middleware 就是檢查訪客「身份」的警衛。它在請求抵達 Controller 之前,為我們提供了一個絕佳的攔截點,來執行權限檢查、日誌紀錄、請求修改等任務。

實戰:打造帶有參數的 Middleware

一個常見的場景是權限控管。我們可能會有 `admin`, `editor`, `viewer` 等多種角色。如果為每個角色都建立一個 Middleware(`IsAdminMiddleware`, `IsEditorMiddleware`…),那也太笨拙了,完全違背了 DRY (Don’t Repeat Yourself) 原則。

更聰明的做法是,建立一個通用的 `CheckUserRole` Middleware,並讓它可以接收參數。讓我們來動手做!

php artisan make:middleware CheckUserRole

接著打開 `app/Http/Middleware/CheckUserRole.php`,修改 `handle` 方法:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CheckUserRole
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string  ...$roles
     * @return mixed
     */
    public function handle(Request $request, Closure $next, ...$roles)
    {
        // 假設我們用 $request->user()->role 來取得使用者角色
        if (! $request->user() || ! in_array($request->user()->role, $roles)) {
            // 如果使用者未登入,或角色不符合,就回傳 403 Forbidden
            abort(403, '權限不足,禁止存取。');
        }

        return $next($request);
    }
}

注意看 `handle` 方法的第三個參數 `…$roles`,這個 `…` (splat operator) 會將傳入的所有參數都收集到 `$roles` 這個陣列中。這樣我們就可以檢查當前使用者的角色是否存在於允許的角色清單中。

接著,到 `app/Http/Kernel.php` 的 `$routeMiddleware` 陣列中註冊這個 Middleware:

'role' => \App\Http\Middleware\CheckUserRole::class,

大功告成!現在我們可以在路由定義中非常彈性地使用它了:

// 只允許 admin 存取
Route::get('/dashboard', [DashboardController::class, 'index'])->middleware('role:admin');

// 允許 admin 或 editor 存取
Route::post('/posts', [PostController::class, 'store'])->middleware('role:admin,editor');

透過 `:` 傳入的參數,會被 Laravel 解析並傳遞給 Middleware 的 `handle` 方法。看,一個 Middleware 就搞定了所有角色的權限檢查,這才是工程師該有的優雅!

結論:寫出讓未來自己感謝的程式碼

今天我們從為什麼需要客製化,一路實作了 Rule 物件、Closure 驗證,以及帶有參數的 Middleware。你會發現,Laravel 框架的彈性遠超你的想像。它不僅提供了方便的工具,更鼓勵你用優雅、可維護的方式去組織你的程式碼。

記住一個原則:Controller 應該只關心「調度」,也就是呼叫哪個 Service、回傳哪個 View 或 JSON。所有關於「資料是否合規」的問題,都交給 FormRequest 和 Validation Rules;所有關於「誰能進來」的問題,都交給 Middleware。當你確實遵守這個職責分離的原則時,你的專案架構會變得非常清晰,擴充功能或除錯時,你都能夠精準地找到對應的程式碼位置,而不是在一個幾百行的 Controller 方法裡大海撈針。

這不只是在寫程式,這是在做「架構設計」。今天多花一點時間思考和打造這些可複用的元件,就是為未來的專案維護省下大把的時間和頭髮。相信我,未來的你,會感謝現在這個願意多走一步的自己。

延伸閱讀

如果你覺得這些觀念對你有幫助,但面對公司複雜的系統,仍然不知道從何下手進行重構或規劃,浪花科技的團隊擁有豐富的 Laravel 與 WordPress 系統開發經驗。我們擅長的不只是把功能做出來,更是打造穩固、高效、可長期維護的系統架構。歡迎點擊這裡,填寫表單與我們聊聊,讓我們用專業的技術為您的事業加分!

常見問題 (FAQ)

Q1: 什麼時候該用 Rule 物件,什麼時候用 Closure 就好?

A: 這是一個很好的問題,關鍵在於「複用性」和「複雜度」。如果一個驗證邏輯很可能會在專案的其他地方(例如另一個 Controller 或 API 端點)被再次使用,或者這個邏輯本身比較複雜(例如需要查詢資料庫、進行多步驟計算),那麼強烈建議使用 Rule 物件。這能讓你的邏輯被封裝起來,方便管理和測試。反之,如果這個驗證邏輯非常簡單,而且你很確定它只會在這個地方出現一次,那麼使用 Closure 會更快速、輕便,可以避免為了單一用途而創建一個新檔案。

Q2: Middleware 和 FormRequest 的驗證有什麼不同?我該用哪個?

A: 他們的職責不同,通常是搭配使用,而不是二選一。Middleware 主要負責「授權 (Authorization)」和「請求過濾」,回答的是「這個使用者『有沒有權限』做這件事?」例如,檢查使用者是否登入、是否具備特定角色。而 FormRequest 的驗證則專注於「資料驗證 (Validation)」,回答的是「使用者送來的『資料格式』是否正確?」例如,Email 格式是否正確、密碼長度是否足夠。一個典型的流程是:請求先通過 Middleware 的權限檢查,確認身份後,再由 FormRequest 驗證資料的正確性,最後才交給 Controller 處理。

Q3: 我可以把多個參數傳給 Middleware 嗎?

A: 當然可以!就像我們文章中的範例一樣,你可以用逗號來分隔多個參數。例如,在路由中定義 ->middleware('role:admin,editor')。在 Middleware 的 `handle` 方法中,使用 `…$roles` 這樣的語法,Laravel 就會自動將 ‘admin’ 和 ‘editor’ 這兩個字串作為一個陣列傳入 `$roles` 變數中,讓你可以輕鬆地在程式碼中進行處理。這對於需要檢查多個權限或條件的場景非常有用。

 
立即諮詢,索取免費1年網站保固