> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 套件的自訂 Session 驅動

> 解讀 Laravel 13 的 SessionManager，說明 SessionHandlerInterface 的儲存契約、明確的驅動選擇、TTL 與序列化、已保留的 Store 以及測試。

想從套件提供自訂的 Session 儲存位置時，請將**儲存處理的註冊**與**應用程式的選擇**分開。設計上應避免僅安裝套件就切換登入狀態的儲存位置，讓使用者在確認對既有 Session 的影響後再啟用。

本頁確認了 Laravel Framework `v13.39.0` 的官方原始碼。基本操作請參閱 [Session](https://kawax.biz/zh-TW/session.md)，提供者的自動載入請參閱[套件自動探索](https://kawax.biz/zh-TW/advanced/package-discovery.md)。

## extend 接收的是處理器而非 Store

`Session::extend()` 的回呼會回傳實作 PHP `SessionHandlerInterface` 的**儲存用處理器**。它並不是直接回傳實作 `Illuminate\Contracts\Session\Session` 的 Store 或快取 Repository 的 API。

`SessionManager::callCustomCreator()` 會將回呼的回傳值傳給 `buildSession()`，並依 `session.encrypt` 以 `Store` 或 `EncryptedStore` 包裝。此時也會套用 Cookie 名稱與 `session.serialization`。

```mermaid theme={null}
flowchart TD
    A["在 boot() 中註冊自訂驅動名稱"] --> B["使用者選擇 session.driver"]
    B --> C["首次呼叫 driver() 時執行建立回呼"]
    C --> D["回傳 SessionHandlerInterface"]
    D --> E["SessionManager 以 Store 或<br>EncryptedStore 包裝"]
    E --> F["StartSession 開始讀取"]
    F --> G["應用程式透過 put() 等操作"]
    G --> H["Store 序列化後<br>傳給 handler.write()"]
```

在一般的 HTTP 處理中，`StartSession` 中介層負責管理 Session 的啟動與儲存。為了提供處理器，套件端不需要呼叫 `session_start()` 或 `session_set_save_handler()`。

## 分開思考儲存契約

實作自訂後端時，需要滿足以下職責。

| 方法 | 處理器的職責 |
| - | - |
| `open($savePath, $sessionName)` | 必要的初始化。不需要時回傳 `true` |
| `close()` | 必要的結束處理。不需要時回傳 `true` |
| `read($sessionId)` | 回傳已儲存的字串。未儲存或已過期時回傳空字串 |
| `write($sessionId, $data)` | 儲存傳入的字串，並回傳成功或失敗 |
| `destroy($sessionId)` | 刪除指定 ID 的資料 |
| `gc($lifetime)` | 依以秒為單位的有效期間刪除過期資料。若儲存位置會自動失效，則不需刪除 |

由於 `Store` 負責序列化與還原，處理器中不要再對 Session 陣列執行 `serialize()` 或 `json_encode()`。啟用加密時，也請直接儲存經 `EncryptedStore` 處理後的字串。

> [!WARNING]
>
> 請區分資料未儲存與後端故障。經確認，`Store::readFromHandler()` 在讀取結果為空或 `false` 時，會將其視為空的 Session。此外，`Store::save()` 不會檢查處理器 `write()` 的回傳值。
>
> 若套件希望一律將故障視為例外，請不要吞掉儲存位置的例外；對於回傳 `false` 的後端，也應在處理器端偵測並轉換為例外，明確訂出方針並加以測試。失敗時 Laravel 不一定會自動重試或通知儲存失敗。

## 在 boot 中只註冊建立處理

以下是重複使用內建 `CacheBasedSessionHandler` 的最小範例。它並非實作新的儲存協定，而是示範如何註冊使用具名快取儲存的自訂驅動。假設 `Acme\SessionBridge\` 透過 Composer 的 PSR-4 對應到 `src/`。

在 `src/SessionBridgeServiceProvider.php` 準備提供者。

```php theme={null}
<?php

namespace Acme\SessionBridge;

use Illuminate\Contracts\Container\Container;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use SessionHandlerInterface;

final class SessionBridgeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Session::extend('acme-cache', function (Container $app): SessionHandlerInterface {
            $config = $app->make('config');
            $store = $config->get('session.store');
            $minutes = (int) $config->get('session.lifetime', 120);

            if (! is_string($store) || $store === '' || $minutes <= 0) {
                throw new InvalidArgumentException(
                    'acme-cache requires a named cache store and a positive session lifetime.'
                );
            }

            return new CacheBasedSessionHandler(
                $app->make('cache')->store($store),
                $minutes,
            );
        });
    }
}
```

`Session::extend()` 會取得 SessionManager 並註冊建立處理，但此時不會解析回呼內的快取儲存。儲存位置要到該驅動首次被選用時才會建立。請不要在提供者的 `boot()` 中執行連線確認或資料儲存。

使用自訂的 `SessionHandlerInterface` 實作時，請替換回呼所回傳的處理器。如果連 `new Store(...)` 都在回呼中完成，就會與 SessionManager 預期的回傳值不符。

* 提供者以一般的非延遲提供者載入。
* 在 Session 首次被使用前完成註冊。若其他提供者會先解析自訂驅動，也要確認載入順序。
* 驅動名稱請使用套件專屬的名稱。同名的 `extend()` 會覆寫建立處理；若註冊內建驅動的名稱，則會比內建實作更優先。

## 由使用端應用程式明確選擇

使用者修改既有 `config/session.php` 中對應的鍵，其他設定保持不變。

```php theme={null}
'driver' => 'acme-cache',
'store' => 'acme-sessions',
```

儲存位置方面，請在 `config/cache.php` 既有的 `stores` 陣列中新增具名儲存。以下是使用 Redis 連線 `default` 的範例。[Redis 的連線設定與所需的用戶端](https://kawax.biz/zh-TW/redis.md)由應用程式端準備。

```php theme={null}
'acme-sessions' => [
    'driver' => 'redis',
    'connection' => 'default',
    'prefix' => 'acme-sessions:',
],
```

此範例的連線與前綴由**快取儲存的設定**決定。由於不會經過內建 Redis Session 驅動的 `createRedisDriver()`，即使新增 `session.connection` 或 `session.prefix`，此回呼也不會自動套用。

此回呼不會變更 Repository 或儲存位置的設定，而是直接使用具名儲存。測試用的 `array` 儲存在同一個應用程式內也會共用相同的儲存區域。

套件不應無條件改寫 `session.driver`、Cookie 名稱或應用程式整體的快取前綴。是否採用不與其他用途共用儲存區域的設計，以及能否讓多台伺服器使用同一個儲存位置，也請與使用者確認。

> [!TIP]
>
> 若目的只是一般的 Redis 或資料庫儲存，請先使用內建的 Session 驅動。自訂驅動是需要新的儲存位置或額外儲存規範時的擴充點。

## 維持 TTL 的單位與儲存格式

`CacheBasedSessionHandler` 的建構子以**分鐘**為單位接收保存期間。`write()` 每次都會將 `$minutes * 60` 傳給快取 Repository 的 `put()`，因此在上面的範例中，每次寫入都會更新 TTL。`gc()` 回傳 `0`，將失效處理交給快取端。

另一方面，`StartSession` 傳給處理器 `gc($lifetime)` 的期間以**秒**為單位。請避免將 `session.lifetime` 的值直接當成秒來儲存。

此外，以下變更不只是修改設定名稱就能解決。

| 變更 | 對既有 Session 的影響 |
| - | - |
| 儲存位置、連線、鍵的前綴 | 舊資料在新的儲存位置中看不到 |
| `session.serialization` | 無法將舊格式的資料以新格式還原 |
| `session.encrypt` | 明文與密文的讀寫不再一致 |
| `session.cookie` | 與瀏覽器送出的既有 Session ID Cookie 的對應關係改變 |

若在套件更新中變更這些項目，請考量重新驗證的提示、是否需要遷移處理，以及新舊版本並存的部署。與[套件的版本相容性管理](https://kawax.biz/zh-TW/advanced/package-versioning.md)相同，儲存格式與設定鍵也應納入需要維護的公開規格。

## 已建立的 Store 不會只因設定變更而被替換

SessionManager 的父類別 `Manager` 會依名稱保留已建立的驅動。此外，`SessionServiceProvider` 會將 `session.store` 註冊為 singleton。

| 操作 | 會發生的事 | 不會發生的事 |
| - | - | - |
| 只變更設定值 | 之後的建立處理能讀到新值 | 已建立處理器的 TTL 或連線變更 |
| 以相同名稱重新註冊 `extend()` | 替換建立回呼 | 捨棄以同名保留的 Store |
| `forgetDrivers()` | 刪除 Manager 內部保留的所有驅動 | 替換已解析的 `session.store` 或外部持有的 Store 參照 |

`forgetDrivers()` 也不是刪除 Session 資料的 API。在執行中的請求裡重新建立 Store，會造成與已讀取的資料或其他參照之間不一致。

此範例的 CacheManager 也會保留具名儲存。若變更了其連線設定，只重新解析 SessionManager 並不足夠。

在使用設定快取的部署中，請重建快取，並重新啟動長時間執行的 worker，讓它載入新的程式碼與設定。應用程式的重新啟動與已儲存資料的遷移是不同的工作。

## Session 儲存與請求鎖是兩回事

僅實作自訂處理器，並不會讓針對同一 Session 的並行請求序列化。`StartSession` 的阻擋功能使用以 `session.block_store` 選擇的快取儲存的鎖，它不一定與 Session 本體的儲存位置相同。

以 `Route::post(...)->block()` 等方式控制時，請另外驗證支援鎖的快取儲存、多台伺服器之間的共用，以及等待時間與持有時間。套件不應擅自啟用應用程式整體的阻擋設定。

## 測試到儲存與重新載入

在使用 [Orchestra Testbench](https://kawax.biz/zh-TW/advanced/package-testing.md) 的套件中，請準備載入提供者並實際選用自訂驅動的測試。以下是 `tests/SessionDriverTest.php` 的範例。測試用的 `array` 快取不使用外部連線。

```php theme={null}
<?php

namespace Acme\SessionBridge\Tests;

use Acme\SessionBridge\SessionBridgeServiceProvider;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Session\Store;
use Orchestra\Testbench\TestCase;

final class SessionDriverTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [SessionBridgeServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('session.driver', 'acme-cache');
        $app['config']->set('session.store', 'session-test');
        $app['config']->set('session.lifetime', 2);
        $app['config']->set('session.cookie', 'acme_session');
        $app['config']->set('session.serialization', 'json');
        $app['config']->set('session.encrypt', false);
        $app['config']->set('cache.stores.session-test', ['driver' => 'array']);
    }

    public function test_it_wraps_the_registered_handler(): void
    {
        $store = $this->app->make('session')->driver();

        $this->assertInstanceOf(Store::class, $store);
        $this->assertInstanceOf(CacheBasedSessionHandler::class, $store->getHandler());
        $this->assertSame($store, $this->app->make('session.store'));
    }

    public function test_it_saves_and_reloads_the_payload(): void
    {
        $manager = $this->app->make('session');
        $first = $manager->driver();
        $first->start();
        $first->put('cart.quantity', 3);
        $first->save();

        $id = $first->getId();
        $payload = $first->getHandler()->read($id);
        $this->assertSame(3, json_decode($payload, true)['cart']['quantity']);

        $manager->forgetDrivers();
        $second = $manager->driver();
        $second->setId($id);
        $second->start();

        $this->assertNotSame($first, $second);
        $this->assertSame(3, $second->get('cart.quantity'));

        $second->invalidate();
        $this->assertSame('', $second->getHandler()->read($id));
    }

    public function test_forgetting_drivers_does_not_replace_the_store_binding(): void
    {
        $old = $this->app->make('session.store');
        $manager = $this->app->make('session');

        $this->app['config']->set('session.lifetime', 5);
        $manager->forgetDrivers();

        $this->assertNotSame($old, $manager->driver());
        $this->assertSame($old, $this->app->make('session.store'));
    }
}
```

此測試是在同一個應用程式內重新建立 Store 的**隔離測試**。它不會驗證 HTTP 中介層、Cookie 往返、跨行程的持久化，也不會驗證真實 Redis 的失效或鎖。請不要將 `array` 當作正式環境的共用儲存位置。

在公開自訂處理器之前，也請加入以下項目。

* 未儲存或已過期的 ID 會回傳空字串
* 儲存的字串能原樣還原，刪除後無法再取得
* TTL 的單位與更新，以及真實後端中的失效
* 讀取、寫入、刪除失敗時，會得到既定的回傳值或例外
* 啟用 `session.encrypt` 時的往返，以及支援的序列化格式
* 僅載入提供者不會存取儲存位置，也不會搶走內建驅動的選擇
* 視需要加入經過真實 `web` 中介層與 Cookie 的 HTTP 測試，以及並行請求的鎖

## 相關頁面

> [card] [Session](https://kawax.biz/zh-TW/session.md)
>
> 確認 Session 的讀寫、快閃資料與 ID 的重新產生。

> [card] [Manager 的內部實作](https://kawax.biz/zh-TW/advanced/manager.md)
>
> 確認自訂驅動的建立與保留，以及對預設驅動的委派。

> [card] [套件的自訂快取儲存](https://kawax.biz/zh-TW/advanced/package-cache-drivers.md)
>
> 確認作為儲存位置的快取 Store 與 Repository 的分工，以及 TTL 契約。

> [card] [套件服務的生命週期](https://kawax.biz/zh-TW/advanced/package-service-lifetimes.md)
>
> 確認長時間執行的 worker 中所保留的相依性與狀態隔離。

## 第一手資料

* [官方 HTTP Session 文件：自訂驅動與阻擋](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/session.md)
* [官方套件開發文件](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/packages.md)
* [SessionManager：自訂建立處理與 Store 的建構](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionManager.php)
* [Store：讀取、序列化、儲存、ID 的重新產生](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Store.php)
* [CacheBasedSessionHandler：TTL 的轉換與儲存契約](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/CacheBasedSessionHandler.php)
* [CacheManager：具名儲存與 Redis 設定](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Manager：extend、driver、forgetDrivers](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Manager.php)
* [SessionServiceProvider：Manager 與 session.store 的 singleton 註冊](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionServiceProvider.php)
* [StartSession：儲存處理、GC、以快取實現的鎖](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Middleware/StartSession.php)
* [官方 CacheBasedSessionHandlerTest：未儲存、TTL、刪除的驗證](https://github.com/laravel/framework/blob/v13.39.0/tests/Session/CacheBasedSessionHandlerTest.php)


## Related topics

- [套件的自訂快取儲存與契約](https://kawax.biz/zh-TW/advanced/package-cache-drivers.md)
- [套件的自訂通知頻道與傳送驗證](https://kawax.biz/zh-TW/advanced/package-notification-channels.md)
- [起始套件（Starter Kit）](https://kawax.biz/zh-TW/starter-kits.md)
- [套件的 Storage 驅動程式註冊與重新解析](https://kawax.biz/zh-TW/advanced/package-filesystem-drivers.md)
- [核心套件與自訂驅動 - Feedable](https://kawax.biz/zh-TW/packages/feedable/core.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.