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

# パッケージのカスタムセッションドライバー

> Laravel 13のSessionManagerを読み解き、SessionHandlerInterfaceの保存契約、明示的なドライバー選択、TTLとシリアライズ、保持済みStoreとテストを解説します。

パッケージから独自のセッション保存先を提供するには、**保存処理の登録**と**アプリケーションによる選択**を分けます。インストールしただけでログイン状態の保存先を切り替えず、利用者が既存セッションへの影響を確認してから有効化できる設計にします。

このページはLaravel Framework `v13.39.0` の公式ソースを確認しています。基本操作は[セッション](https://kawax.biz/jp/session.md)、プロバイダーの自動読み込みは[パッケージの自動検出](https://kawax.biz/jp/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_start()` や `session_set_save_handler()` を呼ぶ必要はありません。

## 保存契約を分けて考える

独自バックエンドを実装する場合は、次の役割を満たします。

| メソッド | ハンドラーの責務 |
| - | - |
| `open($savePath, $sessionName)` | 必要な初期化。不要なら `true` を返す |
| `close()` | 必要な終了処理。不要なら `true` を返す |
| `read($sessionId)` | 保存済みの文字列を返す。未保存・期限切れなら空文字列を返す |
| `write($sessionId, $data)` | 渡された文字列を保存し、成功・失敗を返す |
| `destroy($sessionId)` | 指定IDのデータを削除する |
| `gc($lifetime)` | 秒単位の有効期間に基づいて期限切れデータを削除する。保存先が自動失効するなら削除不要 |

`Store` がシリアライズと復元を担当するため、ハンドラーでセッション配列を再度 `serialize()` や `json_encode()` しません。暗号化を有効にした場合も、`EncryptedStore` が処理した文字列をそのまま保存します。

> [!WARNING]
>
> データ未保存とバックエンド障害は区別します。確認した `Store::readFromHandler()` は、読み込み結果が空や `false` なら空のセッションとして扱います。また、`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が期待する戻り値と合わなくなります。

* プロバイダーは通常の非遅延プロバイダーとして読み込みます。
* セッションが初めて使われるより前に登録を完了させます。別プロバイダーが先に独自ドライバーを解決する場合は読み込み順序も確認します。
* ドライバー名にはパッケージ固有の名前を使います。同名の `extend()` は生成処理を上書きし、組み込みドライバー名を登録すれば組み込み実装より優先されます。

## 利用アプリケーションが明示的に選択する

利用者は既存の `config/session.php` の対応キーを変更します。ほかの設定は残します。

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

保存先には、`config/cache.php` の既存の `stores` 配列へ名前付きストアを追加します。次はRedis接続 `default` を使う例です。[Redisの接続設定と必要なクライアント](https://kawax.biz/jp/redis.md)はアプリケーション側で準備します。

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

この例の接続とプレフィックスは、**キャッシュストアの設定**から決まります。組み込みRedisセッションドライバーの `createRedisDriver()` を通らないため、`session.connection` や `session.prefix` を追加しても、このコールバックが自動で反映するわけではありません。

このコールバックはRepositoryや保存先の設定を変更せず、名前付きストアをそのまま利用します。テスト用の `array` ストアも、同じアプリケーション内では同じ保存領域を共有します。

パッケージから `session.driver`・Cookie名・アプリケーション全体のキャッシュプレフィックスを無条件に書き換えないようにします。ほかの用途と保存領域を共有しない設計や、複数サーバーから同じ保存先を使える構成も利用者と確認します。

> [!TIP]
>
> 通常のRedis・データベース保存だけが目的なら、まず組み込みのセッションドライバーを使います。独自ドライバーは、新しい保存先や追加の保存規約が必要な場合の拡張点です。

## TTLの単位と保存形式を維持する

`CacheBasedSessionHandler` のコンストラクターは、保存期間を**分**で受け取ります。`write()` は毎回 `$minutes * 60` をキャッシュRepositoryの `put()` へ渡すため、上の例では書き込みのたびにTTLを更新します。`gc()` は `0` を返し、失効はキャッシュ側へ委ねます。

一方、`StartSession` がハンドラーの `gc($lifetime)` へ渡す期間は**秒**です。`session.lifetime` の値をそのまま秒として保存する実装にしないようにします。

また、次の変更は設定名の変更だけでは済みません。

| 変更 | 既存セッションへの影響 |
| - | - |
| 保存先・接続・キーのプレフィックス | 旧データが新しい保存先から見えなくなる |
| `session.serialization` | 旧形式のデータを新形式として復元できなくなる |
| `session.encrypt` | 平文と暗号文の読み書きが一致しなくなる |
| `session.cookie` | ブラウザが送る既存のセッションID Cookieとの対応が変わる |

パッケージ更新でこれらを変える場合は、再認証の案内、移行処理の有無、旧版と新版が混在するデプロイを検討します。[パッケージのバージョン互換性管理](https://kawax.biz/jp/advanced/package-versioning.md)と同じく、保存形式と設定キーも保守する公開仕様に含めます。

## 生成済みStoreは設定変更だけでは置き換わらない

SessionManagerの親クラスである `Manager` は、生成したドライバーを名前ごとに保持します。さらに、`SessionServiceProvider` は `session.store` をsingletonとして登録します。

| 操作 | 起きること | 起きないこと |
| - | - | - |
| 設定値だけを変更 | 今後の生成処理が新しい値を読める | 生成済みハンドラーのTTLや接続の変更 |
| 同じ名前で `extend()` を再登録 | 生成コールバックの置換 | 同名で保持済みのStoreの破棄 |
| `forgetDrivers()` | Manager内部の保持済みドライバーをすべて削除 | 解決済み `session.store` や外部が保持するStore参照の差し替え |

`forgetDrivers()` は、セッションデータを削除するAPIでもありません。稼働中リクエストでStoreを作り直すと、すでに読み込んだデータや別の参照との不整合を招きます。

この例のCacheManagerも名前付きストアを保持します。その接続設定を変更した場合は、SessionManagerだけの再解決では不十分です。

設定キャッシュを使うデプロイではキャッシュを再構築し、長寿命ワーカーも新しいコードと設定を読み込むように再起動します。アプリケーションの再起動と保存済みデータの移行は別の作業です。

## セッション保存とリクエストのロックは別

カスタムハンドラーを実装しただけでは、同一セッションへの並列リクエストは直列化されません。`StartSession` のブロッキングは、`session.block_store` で選ぶキャッシュストアのロックを利用します。セッション本体の保存先と同じとは限りません。

`Route::post(...)->block()` などで制御する場合は、ロック対応のキャッシュストア、複数サーバーでの共有、待機時間と保持時間を別途検証します。パッケージがアプリケーション全体のブロッキング設定を勝手に有効化しないようにします。

## 保存・再読み込みまでテストする

[Orchestra Testbench](https://kawax.biz/jp/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] [セッション](https://kawax.biz/jp/session.md)
>
> セッションの読み書き、フラッシュデータ、IDの再生成を確認します。

> [card] [Managerの内部実装](https://kawax.biz/jp/advanced/manager.md)
>
> カスタムドライバーの生成と保持、デフォルトドライバーへの委譲を確認します。

> [card] [パッケージのカスタムキャッシュストア](https://kawax.biz/jp/advanced/package-cache-drivers.md)
>
> 保存先となるキャッシュStoreとRepositoryの分担、TTLの契約を確認します。

> [card] [パッケージサービスのライフサイクル](https://kawax.biz/jp/advanced/package-service-lifetimes.md)
>
> 長寿命ワーカーで保持される依存関係と状態の分離を確認します。

## 一次情報

* [公式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/jp/session.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [パッケージのカスタムキャッシュストアと契約](https://kawax.biz/jp/advanced/package-cache-drivers.md)
- [パッケージのStorageドライバー登録と再解決](https://kawax.biz/jp/advanced/package-filesystem-drivers.md)
- [パッケージのカスタム通知チャネルと配信検証](https://kawax.biz/jp/advanced/package-notification-channels.md)


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