パッケージから独自のキャッシュバックエンドを提供するには、保存処理を実装するStoreと、LaravelのAPIを提供するRepositoryを分けます。Cache::extend() に登録するのは生成方法であり、返したStoreをLaravelが自動でRepositoryに包むわけではありません。
このページはLaravel Framework v13.39.0 の実装を確認しています。基本操作はキャッシュ、プロバイダーの準備はパッケージ開発を参照してください。
ドライバー名とストア名を分ける
CacheManager::build() は、設定の driver に対応するカスタム生成処理があれば、組み込みドライバーより先に呼びます。redis や array などの名前を再利用せず、パッケージ固有の名前を使います。
他のプロバイダーのbootより前に登録する
別のパッケージが boot() でキャッシュを使う場合、自分の boot() でドライバーを登録すると順序次第で間に合いません。公式ドキュメントに沿い、register() でアプリケーションの booting コールバックを登録します。
次は外部サービスなしで登録経路を確認するため、標準の ArrayStore を使う最小例です。Acme\Memo パッケージの src/MemoServiceProvider.php に配置します。
プロバイダーはパッケージの自動検出などで読み込みます。拡張を登録するだけのこのプロバイダーは遅延プロバイダーにせず、HTTP・Artisanの両方で読み込まれるようにします。
- アプリケーションの
booting は、通常の起動で各プロバイダーの register() が完了した後、各 boot() より前に実行されます。プロバイダー自身の $this->booting() とは別です。
extend() のコールバックへはアプリケーションとストア設定配列が渡されます。Storeの生成や外部接続は、この生成用コールバック側に置きます。
- この登録時期は、他のプロバイダーの
register() 内でのキャッシュ利用まで保証しません。サービスの登録と実際の利用を分離してください。
標準のインメモリキャッシュが欲しいだけなら array ドライバーで十分です。この例は登録と契約の説明用で、プロセスをまたいだ共有・永続化には使えません。独自バックエンドでは ArrayStore の生成部分を、Illuminate\Contracts\Cache\Store を実装したクラスへ置き換えます。
CacheManager::extend() は匿名クロージャのスコープをManagerへ再バインドします。非staticクロージャの $this をプロバイダーだと考えず、例のようにstaticクロージャと引数の $app を使います。
利用アプリケーションにストアを追加する
config/cache.php の既存の stores 配列へ追加します。パッケージが利用側の default や stores 全体を書き換える設計にはしません。
デフォルトにしたい場合に CACHE_STORE へ指定するのも、ドライバー名ではなく catalog というストア名です。同じドライバーの設定を複数作れば、ストア名ごとに別のRepositoryが生成されます。
Repositoryは自分で組み立てる
CacheManager::callCustomCreator() はコールバックの戻り値をそのまま返します。Storeだけを返すと、get() や put() が動いても、Repositoryの remember() などは使えず、TTLの解釈も変わります。
例の repository($store, $config) は、次を担当します。
Illuminate\Cache\Repository でStoreを包む。
- Managerが設定に加えた
store 名をRepositoryに渡す。
- イベントDispatcherが登録され、
events 設定が無効でなければ、RepositoryにDispatcherを設定する。
これは独自設定をすべてStoreへ適用する処理ではありません。接続先、prefix、シリアライズなどは、Storeの実装と生成処理側で反映します。例で設定を読み取っている serialize も同様です。
Store契約を現在の実装と照合する
Laravel 13の Illuminate\Contracts\Cache\Store が要求するメソッドを実装します。過去のサンプルの空メソッド一覧を、そのまま動くドライバーとして配布しないでください。
null はRepositoryでキャッシュミスとして扱われます。false や 0 まで未取得扱いにしない実装にします。null を保存しても、remember() の再計算を防ぐ印にはなりません。
TTLはRepositoryを通して検証する
Repository::put() は、Storeへ渡す前にTTLを処理します。
例の ArrayStore は、直接 put() に 0 秒を渡すと無期限として保存します。一方、Repositoryの put(..., 0) は削除です。getStore() で取り出した低レベルStoreだけをテストしても、利用者が呼ぶAPIの動作は確認できません。
add・ロック・タグは別途対応を確認する
Store契約の実装だけで、すべてのキャッシュ機能への対応が保証されるわけではありません。
- 原子的な
add(): Store契約には含まれません。正のTTL付きで呼び、Storeに add() があればRepositoryはそれを使います。なければ get() と put() によるフォールバックなので、Repositoryで包むだけでは原子性を保証できません。共有バックエンドでは、そのバックエンドの原子的な操作で実装し、並行実行も検証します。
- ロック:
Illuminate\Contracts\Cache\LockProvider の lock() と restoreLock() は別の契約です。取得、所有者の一致、期限切れ、他の所有者のロックを解放しないことまで検証します。例の ArrayStore のロックは、別プロセスをまたぐ排他制御にはなりません。
- タグ: RepositoryはStoreに
tags() があるかを確認します。一般的には TaggableStore などを使った実装が必要で、Store契約だけで自動的に有効にはなりません。
flush() は設定のprefixを尊重した部分削除だと考えないでください。共有バックエンドでの削除範囲を確認し、php artisan cache:clear も含めて利用者へ説明します。
インスタンスの保持とデータの削除を区別する
CacheManager はsingletonで、store('catalog') の結果もストア名ごとに保持します。設定変更や extend() の再登録だけでは、生成済みRepositoryは更新されません。
$same は $before と同じオブジェクトです。forgetDriver() の後の取得で、新しい設定から $after が生成されます。
forgetDriver() はバックエンドの全削除ではありません。ただし例の ArrayStore はインスタンス内にデータを持つため、新しいStoreから古いデータは見えません。また、利用側の $before や、解決済みの cache.store singletonが保持する古いRepositoryの参照は差し替わりません。
本番では通常のデプロイに沿って設定キャッシュを更新し、長寿命ワーカーを再起動します。サービスのライフサイクルも考慮し、再解決を全プロセスの一括更新として扱わないでください。
ファサードのモックだけでなく生成経路をテストする
パッケージのテスト環境で、実際のManagerとRepositoryを通します。次のTestbench例は、先に登録した別のプロバイダーの boot() でもドライバーを利用できることを確認します。例では cache.serializable_classes の復元制限を変更せずに配列を保存し、ArrayStore::all(false) で生の保存値も確認します。
独自バックエンドでは、これに加えて期限切れ、many() の未取得キー、false・0 の往復、touch()、接続障害、並行更新、prefixと削除範囲を検証します。単なる Cache::shouldReceive('get') の成功を、ドライバー登録や外部サービスとの互換性の証拠にはしません。
長期保守のチェックリスト
- ドライバー名、ストア設定、シリアライズ形式、例外と失敗時の戻り値を公開仕様にする。
- Store契約のメソッドと、タグ・ロックなどの任意機能を対応Laravelバージョンごとに確認する。
- 設定キャッシュありの新規起動でも登録でき、他のプロバイダーの
boot() から利用できることを検証する。
- 保存データの互換性変更は、既存キーの移行や破棄の方針まで含めて告知する。
- プロセス内のインスタンス保持、バックエンドのデータ、設定キャッシュを区別する。
参照した一次情報