> ## 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()` 的是创建方式，Laravel 并不会自动将返回的 Store 包装成 Repository。

本页面基于对 Laravel Framework `v13.39.0` 实现的确认。基本操作请参阅[缓存](https://kawax.biz/zh-CN/cache.md)，服务提供者的准备请参阅[包开发](https://kawax.biz/zh-CN/advanced/package-development.md)。

## 区分驱动名称与存储名称

| 要素 | 本页示例 | 作用 |
| - | - | - |
| 驱动名称 | `acme-memory` | 通过 `extend()` 注册的创建方式 |
| 存储名称 | `catalog` | 在 `cache.stores` 中定义的配置，以及已创建 Repository 的标识符 |
| Store | `Illuminate\Cache\ArrayStore` | 键的保存、读取与过期时间管理 |
| Repository | `Illuminate\Cache\Repository` | `remember()`、TTL 转换、缓存事件等 |

如果配置的 `driver` 存在对应的自定义创建处理，`CacheManager::build()` 会优先于内置驱动调用它。不要复用 `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/zh-CN/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()` 重新计算的标记。

### 通过 Repository 验证 TTL

`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')` | 删除已保存的单个键 |
| `Cache::store('catalog')->flush()` | 删除 Store 的全部缓存数据 |

`forgetDriver()` 并不是对后端的全部删除。不过示例中的 `ArrayStore` 将数据保存在实例内部，因此新的 Store 看不到旧数据。此外，使用方持有的 `$before`，以及已解析的 `cache.store` singleton 所持有的旧 Repository 引用都不会被替换。

在生产环境中，请按照常规部署流程更新配置缓存，并重启长期运行的 worker。还要考虑[服务的生命周期](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)，不要将重新解析视为对所有进程的统一更新。

## 不只 mock 门面，还要测试创建路径

在[包的测试环境](https://kawax.biz/zh-CN/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')` 成功当作驱动注册或与外部服务兼容的证据。

## 长期维护检查清单

* 将驱动名称、存储配置、序列化格式、异常及失败时的返回值作为公开规范。
* 针对每个支持的 Laravel 版本，确认 Store 契约的方法以及标签、锁等可选功能。
* 验证在启用配置缓存的全新启动中也能完成注册，并能在其他服务提供者的 `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/zh-CN/advanced/index.md)
- [缓存](https://kawax.biz/zh-CN/cache.md)
- [创建 Boost 的自定义 Agent](https://kawax.biz/zh-CN/advanced/boost-custom-agent.md)
- [创建 AI SDK 的自定义 Provider](https://kawax.biz/zh-CN/advanced/ai-sdk-custom-provider.md)
- [限流的自定义](https://kawax.biz/zh-CN/advanced/rate-limiting.md)


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