停止讓「Session」擾亂您的系統穩定度!

小故事

小明在維護一個多網域平台(例如:promo-a.example.compromo-b.example.com),每個子域對應不同的合作廠商或活動行為(以下統稱「行為」)。前端頁面會根據當前行為顯示對應的 logo、Banner 與促銷資料。系統的第一道邏輯是根據瀏覽器請求的 host 去查資料庫拿到對應的行為,並把行為 id 放到 session(或 request-scoped context)供後續 view 使用。

有一次 PM 回報:部分使用者在特定子域看到錯誤的 logo(顯示別的廠商 logo)。追查紀錄發現,當 middleware 尚未把行為寫入 session(或 service)前,某些 view-composer 或 early booted code 已經去讀 session 並查 DB,導致畫面展現錯誤的資料。


常見的錯誤模式:

  • 多網域或代理轉發時,第一道處理邏輯應該根據 host 決定行為並設置 context(session/request service)。
  • 若把設定 context 的責任分散(有人在 controller、有人在 service provider、有人在 view composer),就會出現時序不一致的情況。
  • 當某個共用程式(例如 view composer、service provider boot)在 middleware 前執行並讀 session,就會拿到舊值或 null,導致隨機的畫面錯誤(例如 logo 顯示錯誤)。

在開發複雜的 Web 應用程式,尤其面對多網域、多廠商或多租戶(Multi-tenancy)的平台時,工程師經常面臨一個棘手的問題:系統狀態的不穩定性

例如,當使用者訪問 promo-a.example.com 時,頁面偶爾會顯示成 promo-b 廠商的 Logo 或資料。這類錯誤看似隨機,追根究柢,往往是程式碼在處理「請求上下文」(Request Context)時,讀取狀態的時機錯誤 所致。

本文將根據「狀態一致性與請求上下文管理」的通用解法,提供一套嚴謹的 狀態護城河 策略,確保系統的穩定性並大幅提高可測試性。

核心問題:狀態的時序錯誤

當系統需要根據當前請求(如 hostsubdomain)來決定應用程式的行為 ID (behavior_id) 時,我們通常會將這個 ID 儲存在 Session 或 Request Scope 中。

常見的錯誤模式 在於將設定 Context 的責任分散,導致某些共用程式碼(如 AppServiceProvider::boot 階段,或某些 View Composer)在 Middleware 尚未 寫入正確的 behavior_id 到 Session 之前,就提早去讀取了舊值或 null

錯誤示範:過早讀取 Session

如果在 Service Provider 的 boot 階段執行需要依賴 Context 的邏輯,將會導致資料錯亂:

PHP
// app/Providers/AppServiceProvider.php
// ❌ 問題:在 boot 階段讀 Session,可能會得到 null 或失準的值
public function boot()
{
    $behaviorId = Session::get('behavior_id');

    // ❌ 問題:直接依賴 $behaviorId 去查資料並 share,會把錯資料共享給所有 view
    $headerBlogs = Blog::where('behavior_id', $behaviorId)
        ->where('position', 'header')
        ->get();

    View::share('headerBlogs', $headerBlogs);
}

這會將錯誤的資料共享給所有 View,造成畫面資料不一致。


通用解法:建立狀態一致性的三大核心策略

為了解決這個時序問題,我們必須將狀態管理集中化,並導入 單一真相來源(SSOT) 的概念。

策略一:單一真相來源 (SSOT) – BehaviorService

所有與請求上下文相關的讀取與寫入,必須透過一個專屬的 Service 類別來封裝,讓這個 Service 成為 唯一 會接觸到行為狀態的介面。

程式碼佐證:BehaviorService 範例

PHP
// app/Services/BehaviorService.php
namespace App\Services;

use Illuminate\Support\Facades\Session;

class BehaviorService
{
    public function getBehaviorId(): ?int
    {
        return Session::get('behavior_id');
    }

    public function setBehaviorId(int $behaviorId): void
    {
        // 唯一寫入點
        Session::put('behavior_id', $behaviorId);
    }

    public function hasBehaviorId(): bool
    {
        // 提供檢查方法
        return Session::has('behavior_id') && !is_null($this->getBehaviorId());
    }

    // ... clearBehaviorId() 等方法略
}

這個 Service 應設計成可被 Mock 的,這對於撰寫功能測試時,能輕鬆模擬不同行為情境至關重要。

策略二:在正確時機透過 Middleware 設置狀態

Middleware 是處理請求上下文的最佳場所。我們需要確保狀態設置 Middleware (SetBehaviorContext) 註冊在 StartSession 之後,但所有需要使用該狀態的 Controller 或 View Composer 之前。

程式碼佐證:SetBehaviorContext Middleware 範例

PHP
// app/Http/Middleware/SetBehaviorContext.php
// ... 略過命名空間與引用

class SetBehaviorContext
{
    // 透過建構子注入 Service
    public function __construct(BehaviorService $behaviorService, BlogService $blogService)
    {
        $this->behaviorService = $behaviorService;
        $this->blogService = $blogService;
    }

    public function handle(Request $request, Closure $next)
    {
        $host = $request->getHost();
        $behavior = /* 根據 $host 查詢 Behavior Model */;

        // 1. 唯一寫入點:在 Middleware 設置 Context
        $this->behaviorService->setBehaviorId($behavior->id);

        // 2. 設置完畢後,安全地載入並共享 View 資料
        $this->shareBlogData();

        return $next($request);
    }

    protected function shareBlogData(): void
    {
        // 這裡會呼叫策略三中帶有 Guard 的 Service
        $headerBlogs = $this->blogService->findByPosition('header');
        View::share('headerBlogs', $headerBlogs);
        // ... 略
    }
}

如此一來,任何後續的程式碼(Controller、View Composer 等)都能保證讀取到已就緒的最新狀態。

策略三:Guard – 讀取端設置防護機制

所有依賴 BehaviorService 狀態的讀取端,必須先執行 Guard 檢查。若 Context 尚未建立或不存在,Service 應回傳空的集合或合理的預設值,而不是允許資料庫查詢發生錯誤。

程式碼佐證:BlogService Guard 範例

PHP
// app/Services/BlogService.php
// ... 略過命名空間與引用

class BlogService
{
    protected $behaviorService;

    public function __construct(BehaviorService $behaviorService)
    {
        $this->behaviorService = $behaviorService;
    }

    public function findByPosition(string $position): Collection
    {
        // ✅ Guard:檢查狀態是否存在
        if (! $this->behaviorService->hasBehaviorId()) {
            return collect(); // 回傳安全預設值
        }

        $behaviorId = $this->behaviorService->getBehaviorId();

        // 執行查詢
        return Blog::where('behavior_id', $behaviorId)
            // ... 略
            ->get();
    }
}

這個 Guard 機制保護了系統,使其免於任何時序錯誤的影響。

告別系統「間歇性錯亂」:用 Middleware 建立狀態一致性的「單一真相來源」

將狀態管理提升到系統級別的流程,不僅解決了狀態不一致的問題,更為開發帶來長期的效益:

  1. 提升測試便利性: 集中化的 BehaviorService可被 Mock 的關鍵。在進行高階行為測試(Feature Test)時,工程師可以精準地模擬不同的行為 ID,確保測試結果不會因為 Session 殘留或狀態不穩而出現偏差。這讓測試更貼近實際業務邏輯,並確保了資料庫互動的一致性。
  2. 更清晰的責任劃分: 整個系統對「行為 ID」的依賴變得可預期、更容易除錯(Debug)。
  3. 建立系統防禦機制: 這種嚴謹的上下文管理流程,成為了系統的「自動化防禦機制」,能夠在程式發生錯誤時及時拉響警報。

正如開發經驗所得,寫測試能促使我們將程式碼拆分為更容易被呼叫、被管理的模組,而狀態一致性的處理,正是確保這些模組在運行時能夠穩定互動的基礎。

透過建立這套狀態管理機制,我們學會了如何在應用層建立穩定、可測試的架構。

Ad Feature

這裡可以輸入標題、內文,也可以做粗體和斜體的變化,也可以插入圖片,或者是用 iframe 語法做任何操作也是可以的,如不需要可至精選功能 → 文末自訂廣告刪除。

分享此內容: