> ## 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のCacheManagerとStore契約を読み解き、ドライバーの登録時期、RepositoryによるTTL変換、インスタンス保持、ロックなどの対応範囲を検証します。

パッケージから独自のキャッシュバックエンドを提供するには、**保存処理を実装するStore**と、**LaravelのAPIを提供するRepository**を分けます。`Cache::extend()` に登録するのは生成方法であり、返したStoreをLaravelが自動でRepositoryに包むわけではありません。

このページはLaravel Framework `v13.39.0` の実装を確認しています。基本操作は[キャッシュ](https://kawax.biz/jp/cache.md)、プロバイダーの準備は[パッケージ開発](https://kawax.biz/jp/advanced/package-development.md)を参照してください。

## ドライバー名とストア名を分ける

| 要素 | このページの例 | 役割 |
| - | - | - |
| ドライバー名 | `acme-memory` | `extend()` で登録する生成方法 |
| ストア名 | `catalog` | `cache.stores` に定義する設定と、生成済みRepositoryの識別子 |
| Store | `Illuminate\Cache\ArrayStore` | キーの保存、取得、有効期限の管理 |
| Repository | `Illuminate\Cache\Repository` | `remember()`、TTLの変換、キャッシュイベントなど |

`CacheManager::build()` は、設定の `driver` に対応するカスタム生成処理があれば、組み込みドライバーより先に呼びます。`redis` や `array` などの名前を再利用せず、パッケージ固有の名前を使います。

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"catalogは解決済み？"}
    B -->|はい| C["保持済みのRepositoryを返す"]
    B -->|いいえ| D["cache.stores.catalogを読み込む"]
    D --> E["driver: acme-memoryの生成処理"]
    E --> F["Storeを生成し<br>repository()で包む"]
    F --> G["catalogの名前で保持"]
```

## 他のプロバイダーのbootより前に登録する

別のパッケージが `boot()` でキャッシュを使う場合、自分の `boot()` でドライバーを登録すると順序次第で間に合いません。公式ドキュメントに沿い、`register()` で**アプリケーションの `booting` コールバック**を登録します。

次は外部サービスなしで登録経路を確認するため、標準の `ArrayStore` を使う最小例です。`Acme\Memo` パッケージの `src/MemoServiceProvider.php` に配置します。

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

namespace Acme\Memo;

use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;

class MemoServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->booting(static function (Application $app): void {
            $app['cache']->extend('acme-memory', static function (
                Application $app,
                array $config,
            ): Repository {
                $store = new ArrayStore(
                    $config['serialize'] ?? false,
                    $app['config']->get('cache.serializable_classes'),
                );

                return $app['cache']->repository($store, $config);
            });
        });
    }
}
```

プロバイダーは[パッケージの自動検出](https://kawax.biz/jp/advanced/package-discovery.md)などで読み込みます。拡張を登録するだけのこのプロバイダーは遅延プロバイダーにせず、HTTP・Artisanの両方で読み込まれるようにします。

* アプリケーションの `booting` は、通常の起動で各プロバイダーの `register()` が完了した後、各 `boot()` より前に実行されます。プロバイダー自身の `$this->booting()` とは別です。
* `extend()` のコールバックへはアプリケーションとストア設定配列が渡されます。Storeの生成や外部接続は、この生成用コールバック側に置きます。
* この登録時期は、他のプロバイダーの **`register()` 内でのキャッシュ利用**まで保証しません。サービスの登録と実際の利用を分離してください。

> [!TIP]
>
> 標準のインメモリキャッシュが欲しいだけなら `array` ドライバーで十分です。この例は登録と契約の説明用で、プロセスをまたいだ共有・永続化には使えません。独自バックエンドでは `ArrayStore` の生成部分を、`Illuminate\Contracts\Cache\Store` を実装したクラスへ置き換えます。

`CacheManager::extend()` は匿名クロージャのスコープをManagerへ再バインドします。非staticクロージャの `$this` をプロバイダーだと考えず、例のようにstaticクロージャと引数の `$app` を使います。

### 利用アプリケーションにストアを追加する

`config/cache.php` の既存の `stores` 配列へ追加します。パッケージが利用側の `default` や `stores` 全体を書き換える設計にはしません。

```php theme={null}
'catalog' => [
    'driver' => 'acme-memory',
    'serialize' => true,
],
```

```php theme={null}
use Illuminate\Support\Facades\Cache;

$value = Cache::store('catalog')->remember('product:42', 60, function () {
    return ['id' => 42, 'name' => 'Sample'];
});
```

デフォルトにしたい場合に `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` が要求するメソッドを実装します。過去のサンプルの空メソッド一覧を、そのまま動くドライバーとして配布しないでください。

| メソッド | 確認する振る舞い |
| - | - |
| `get()` / `many()` | 未取得・期限切れは `null`。`many()` は要求したキーをキーに持つ配列を返し、未取得分も `null` にする |
| `put()` / `putMany()` | 秒単位のTTLで保存し、成功・失敗を返す |
| `forever()` | 有効期限を設けずに保存する。バックエンドによる退避・削除まで防ぐ意味ではない |
| `touch()` | 存在するキーの有効期限を変更する。未取得・期限切れなら `false` を返す |
| `increment()` / `decrement()` | 更新後の値または失敗を返す。並行更新とTTL維持も検証する |
| `forget()` | キーを削除し、結果を返す |
| `flush()` | キャッシュ全体を削除する。実際に消える範囲を明示する |
| `getPrefix()` | Storeが使うキープレフィックスを返す |

`null` はRepositoryでキャッシュミスとして扱われます。`false` や `0` まで未取得扱いにしない実装にします。`null` を保存しても、`remember()` の再計算を防ぐ印にはなりません。

### TTLはRepositoryを通して検証する

`Repository::put()` は、Storeへ渡す前にTTLを処理します。

| Repositoryへの呼び出し | Storeへの処理 |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | 秒数を渡した `put()` |
| `put('key', $value, $expiresAt)` | 日時などから秒数へ変換し、正なら `put()` |
| `put('key', $value, 0)` または負のTTL | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` または負のTTL | `forget()` |

> [!WARNING]
>
> 例の `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は更新されません。

```php theme={null}
use Illuminate\Support\Facades\Cache;

$before = Cache::store('catalog');

config(['cache.stores.catalog.serialize' => false]);

$same = Cache::store('catalog');

Cache::forgetDriver('catalog');

$after = Cache::store('catalog');
```

`$same` は `$before` と同じオブジェクトです。`forgetDriver()` の後の取得で、新しい設定から `$after` が生成されます。

| 操作 | 対象 |
| - | - |
| `Cache::forgetDriver('catalog')` | Managerが保持するRepositoryの参照を取り除く |
| `Cache::store('catalog')->forget('key')` | 保存された1キーを削除する |
| `Cache::store('catalog')->flush()` | Storeのキャッシュデータを全削除する |

`forgetDriver()` はバックエンドの全削除ではありません。ただし例の `ArrayStore` はインスタンス内にデータを持つため、新しいStoreから古いデータは見えません。また、利用側の `$before` や、解決済みの `cache.store` singletonが保持する古いRepositoryの参照は差し替わりません。

本番では通常のデプロイに沿って設定キャッシュを更新し、長寿命ワーカーを再起動します。[サービスのライフサイクル](https://kawax.biz/jp/advanced/package-service-lifetimes.md)も考慮し、再解決を全プロセスの一括更新として扱わないでください。

## ファサードのモックだけでなく生成経路をテストする

[パッケージのテスト環境](https://kawax.biz/jp/advanced/package-testing.md)で、実際のManagerとRepositoryを通します。次のTestbench例は、先に登録した別のプロバイダーの `boot()` でもドライバーを利用できることを確認します。例では `cache.serializable_classes` の復元制限を変更せずに配列を保存し、`ArrayStore::all(false)` で生の保存値も確認します。

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

namespace Acme\Memo\Tests;

use Acme\Memo\MemoServiceProvider;
use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Orchestra\Testbench\TestCase;

class CacheConsumerProvider extends ServiceProvider
{
    public function boot(): void
    {
        Cache::store('catalog')->forever('boot-marker', 'ready');
    }
}

class PackageCacheTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [CacheConsumerProvider::class, MemoServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('cache.default', 'array');
        $app['config']->set('cache.stores.catalog', [
            'driver' => 'acme-memory',
            'serialize' => true,
        ]);
    }

    public function test_the_driver_is_available_during_another_providers_boot(): void
    {
        $cache = Cache::store('catalog');

        $this->assertInstanceOf(Repository::class, $cache);
        $this->assertInstanceOf(ArrayStore::class, $cache->getStore());
        $this->assertSame('catalog', $cache->getName());
        $this->assertSame('ready', $cache->get('boot-marker'));
        $this->assertNotNull($cache->getEventDispatcher());
    }

    public function test_serialization_is_applied_from_the_store_configuration(): void
    {
        $cache = Cache::store('catalog');
        $value = ['status' => 'queued'];

        $cache->put('job', $value, 60);

        $this->assertSame($value, $cache->get('job'));
        $this->assertSame(serialize($value), $cache->getStore()->all(false)['job']['value']);
    }

    public function test_a_zero_ttl_deletes_the_item_through_the_repository(): void
    {
        $cache = Cache::store('catalog');
        $cache->forever('product:42', 'old');

        $this->assertTrue($cache->put('product:42', 'new', 0));
        $this->assertNull($cache->get('product:42'));
    }

    public function test_forgetting_a_driver_rebuilds_only_the_manager_reference(): void
    {
        $before = Cache::store('catalog');

        config(['cache.stores.catalog.serialize' => false]);
        $this->assertSame($before, Cache::store('catalog'));

        Cache::forgetDriver('catalog');
        $after = Cache::store('catalog');

        $this->assertNotSame($before, $after);
        $this->assertSame('ready', $before->get('boot-marker'));
        $this->assertNull($after->get('boot-marker'));

        $value = ['status' => 'queued'];
        $after->forever('job', $value);

        $this->assertSame($value, $after->get('job'));
        $this->assertSame($value, $after->getStore()->all(false)['job']['value']);
    }
}
```

独自バックエンドでは、これに加えて期限切れ、`many()` の未取得キー、`false`・`0` の往復、`touch()`、接続障害、並行更新、prefixと削除範囲を検証します。単なる `Cache::shouldReceive('get')` の成功を、ドライバー登録や外部サービスとの互換性の証拠にはしません。

## 長期保守のチェックリスト

* ドライバー名、ストア設定、シリアライズ形式、例外と失敗時の戻り値を公開仕様にする。
* Store契約のメソッドと、タグ・ロックなどの任意機能を対応Laravelバージョンごとに確認する。
* 設定キャッシュありの新規起動でも登録でき、他のプロバイダーの `boot()` から利用できることを検証する。
* 保存データの互換性変更は、既存キーの移行や破棄の方針まで含めて告知する。
* プロセス内のインスタンス保持、バックエンドのデータ、設定キャッシュを区別する。

## 参照した一次情報

* [Laravel公式ドキュメント: カスタムキャッシュドライバー](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/cache.md#adding-custom-cache-drivers)
* [CacheManager: build・repository・extend・forgetDriver](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Repository: TTL変換・add・tags・イベント](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Store契約](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [LockProvider契約](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore: 保存・有効期限・シリアライズ](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider: Managerとcache.storeのsingleton登録](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application: アプリケーションのbootingとプロバイダーのboot](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Laravel公式テスト: CacheManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Cache/CacheManagerTest.php) / [CacheRepositoryTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Cache/CacheRepositoryTest.php)


## Related topics

- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [キャッシュ](https://kawax.biz/jp/cache.md)
- [Eloquentのカスタムキャスト](https://kawax.biz/jp/advanced/eloquent-casts.md)
- [パッケージのキャッシュとoptimizeへの統合](https://kawax.biz/jp/advanced/package-optimization.md)
- [パッケージのルート登録とキャッシュ](https://kawax.biz/jp/advanced/package-routes.md)


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