跳转到主要内容
要从包中提供自己的缓存后端,应将实现存储处理的 Store 与提供 Laravel API 的 Repository 分离开来。注册到 Cache::extend() 的是创建方式,Laravel 并不会自动将返回的 Store 包装成 Repository。 本页面基于对 Laravel Framework v13.39.0 实现的确认。基本操作请参阅缓存,服务提供者的准备请参阅包开发。

区分驱动名称与存储名称

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

通过 Repository 验证 TTL

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 引用都不会被替换。 在生产环境中,请按照常规部署流程更新配置缓存,并重启长期运行的 worker。还要考虑服务的生命周期,不要将重新解析视为对所有进程的统一更新。

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

在包的测试环境中,让测试经过真实的 Manager 和 Repository。下面的 Testbench 示例确认了在先注册的另一个服务提供者的 boot() 中也能使用该驱动。示例在不更改 cache.serializable_classes 还原限制的前提下保存数组,并通过 ArrayStore::all(false) 确认原始保存值。
对于自定义后端,除此之外还需验证过期、many() 的未命中键、false 和 0 的往返、touch()、连接故障、并发更新,以及 prefix 与删除范围。不要将简单的 Cache::shouldReceive('get') 成功当作驱动注册或与外部服务兼容的证据。

长期维护检查清单

  • 将驱动名称、存储配置、序列化格式、异常及失败时的返回值作为公开规范。
  • 针对每个支持的 Laravel 版本,确认 Store 契约的方法以及标签、锁等可选功能。
  • 验证在启用配置缓存的全新启动中也能完成注册,并能在其他服务提供者的 boot() 中使用。
  • 对保存数据的兼容性变更,应连同现有键的迁移或废弃方针一并公告。
  • 区分进程内的实例保留、后端的数据以及配置缓存。

参考的一手资料

最后修改于 2026年10月29日