PSR-6 缓存接口设计全解析:从 Meta 文档看 PHP-FIG 如何统一缓存生态
发布时间:2026/10/6 2:09:36 作者:尧图编辑部 阅读量:1,286

文档开发工具【免费下载链接】fig-standardsStandards either proposed or approved by the Framework Interop Group项目地址https://gitcode.com/gh_mirrors/fi/fig-standards点击查看免费下载缓存是 PHP 项目提升性能最常用的手段之一然而在 PSR-6 出现之前每个框架和库都在“重复造轮子”有的自己实现一套缓存有的为不同缓存后端编写大量适配器导致开发者被迫学习多种互不兼容的缓存 API。本文以 PHP-FIGFramework Interop Group仓库中已接受的 PSR-6 缓存接口 Meta 文档 为骨架结合 PSR-6 正式规范 的接口定义与 PSR 工作流章程 的流程约束深入剖析这一标准背后的设计动机、核心架构决策、被否决的替代方案以及发布后通过 Errata 修补的类型演进历程。读完本文你将完整掌握 PSR-6 的“仓储模型”设计思路、CacheItem 与 Pool 的正确用法以及 psr/cache 包 2.0/3.0 的兼容性边界。1. 背景缓存生态的碎片化问题Meta 文档开篇Summary直指一个核心痛点缓存几乎无处不在但大多数库都在各自实现一套缓存机制且功能层次参差不齐。这造成两个连锁问题调用方库/框架的开发者被迫学习多个缓存系统而这些系统未必提供他们需要的全部功能实现方缓存库开发者面临两难要么只支持有限的几个框架要么为大量框架各写一套适配器adapter class。文档给出的解法非常简单直白为缓存系统定义一个通用接口。库与框架开发者可以信赖缓存系统按预期方式工作缓存系统开发者只需实现一套接口而不必为每个消费方各写适配器。同时这一设计被明确要求“面向未来可扩展”——允许各种内部实现不同但 API 兼容的方案共存并为后续 PSR 或具体实现方预留清晰的扩展路径详见第 3 节 Approaches。值得注意PSR-6 在仓库 PSR.md 的索引表中状态为Accepted由 Larry Garfield 担任维护者EditorMeta 文档第 5 节记录其赞助人为 PPI Framework 协调人 Paul Dragoonis 与 Stash 缓存库作者 Robert Hafner——这再次印证了规范制定过程中“实现方参与设计”的务实路线。2. 设计目标与边界ScopeMeta 文档第 3 节明确划定了 PSR-6 的目标与明确不做的范围Goals目标为基础和中级缓存需求提供通用接口提供清晰的扩展机制支持未来 PSR 或单个实现方扩展高级特性且允许多个独立扩展互不冲突。Non-Goals非目标不与所有现有缓存实现保持架构级兼容不包含命名空间namespacing或标签tagging这类仅少数用户使用的高级缓存特性。这两条边界非常重要它决定了 PSR-6 是一个“够用且可扩展”的底线标准而不是一个包罗万象的超集。标签、命名空间等功能被明确排除在规范之外但如第 4 节所示通过接口扩展可以无冲突地叠加见下方 Taggable 示例。3. 核心设计决策仓储模型Repository / Data Mapper3.1 为什么放弃传统的“可过期键值对”模型Meta 文档第 4.1 节明确PSR-6采用“仓储模型 / 数据映射器模型”repository model / data mapper model而非更传统的“可过期键值对”模型首要理由是灵活性——简单的 key/value 模型“更难扩展”。该模型强制引入两个核心对象CacheItem代表一条缓存条目一个 key/value 对Pool代表一块给定的缓存数据存储区。使用流程是Item 从 Pool 中被取出 → 与 Item 交互 → 将 Item 交还 Pool。文档坦承这种写法“有时略显啰嗦”但在缓存场景比单纯“保存/取回一个字符串”更复杂时它能提供稳健、灵活的方案。方法名则来自对成员项目及其他流行非成员系统的调查问卷第 7 节 Relevant Links 收录了这份调查。对照 PSR-6 正式规范 的“Key Concepts”一节可以确认这一模型的落点Pool是缓存系统中条目的集合是逻辑上的 Repository所有可缓存条目都以 Item 对象形式从 Pool 取出一切与缓存对象的交互都经由 Pool 完成Item是 Pool 内单个 key/value 对key 必须不可变value 可随时更改规范同时强调调用方MUST NOT 自行实例化 Item只能通过 Pool 的getItem()获取且不应假设一个实现创建的 Item 与另一个实现的 Pool 兼容。3.2 模型带来的实际收益Meta 文档列出该方案的优劣势维度说明Pros灵活且可扩展允许实现方式千差万别而不违反接口不会把对象构造函数隐式暴露成一种“伪接口”Cons比朴素写法稍显啰嗦其中“不隐式暴露构造函数”这一点直接针对被否决的“弱项”方案见第 5.1 节——如果调用方必须自己new CacheItem(...)那么 Item 的构造函数签名就成了事实上的接口约束任何想给 Item 增加智能行为的实现都会寸步难行。4. 典型使用模式五个实战示例Meta 文档第 4.1 节末尾给出了一组非规范性non-normative但极具教学价值的“widget 列表”示例完整展示了 PSR-6 的六类核心操作。以下全部代码直接来自文档并结合 PSR-6 规范 的接口语义逐段解读。4.1 读取并回填getItemisHitsetsave/** * Gets a list of available widgets. * * In this case, we assume the widget list changes so rarely that we want * the list cached forever until an explicit clear. */ function get_widget_list() { $pool get_cache_pool(widgets); $item $pool-getItem(widget_list); if (!$item-isHit()) { $value compute_expensive_widget_list(); $item-set($value); $pool-save($item); } return $item-get(); }这是 PSR-6 最经典的“先查后写”模式getItem()在缓存未命中时也必须返回一个 Item 对象不得返回 null用isHit()判断命中与否——因为null本身是合法的缓存值绝不能靠get()的返回值是否为 null 来判断未命中未命中时计算昂贵结果、set()写入、save()立即持久化。4.2 无条件覆盖写入setsave/** * Caches a list of available widgets. * * In this case, we assume a list of widgets has been computed and we want * to cache it, regardless of what may already be cached. */ function save_widget_list($list) { $pool get_cache_pool(widgets); $item $pool-getItem(widget_list); $item-set($list); $pool-save($item); }写入模式无需关心现有缓存set()之后save()立即持久化。这里save()的返回值bool表示持久化是否成功规范允许实现方因底层错误返回 false但不应让缓存错误导致应用崩溃见第 6 节错误处理。4.3 精确删除deleteItems/** * Clears the list of available widgets. * * In this case, we simply want to remove the widget list from the cache. We * dont care if it was set or not; the post condition is simply no longer set. */ function clear_widget_list() { $pool get_cache_pool(widgets); $pool-deleteItems([widget_list]); }注意后置条件语义删除不存在的 key 不是错误规范明确“删除后该 key 不存在或池为空”这一后置条件相同故不构成错误条件。deleteItems()接受 key 数组进行批量删除。4.4 清空整池clear/** * Clears all widget information. * * In this case, we want to empty the entire widget pool. There may be other * pools in the application that will be unaffected. */ function clear_widget_cache() { $pool get_cache_pool(widgets); $pool-clear(); }clear()只清空当前 Pool应用中的其他 Pool 不受影响——这正是“池是隔离的存储区”这一仓储模型语义的体现。4.5 批量加载 延迟持久化getItemssaveDeferredcommit/** * Load widgets. * * We want to get back a list of widgets, of which some are cached and some * are not. This of course assumes that loading from the cache is faster than * whatever the non-cached loading mechanism is. * * In this case, we assume widgets may change frequently so we only allow them * to be cached for an hour (3600 seconds). We also cache newly-loaded objects * back to the pool en masse. * * Note that a real implementation would probably also want a multi-load * operation for widgets, but thats irrelevant for this demonstration. */ function load_widgets(array $ids) { $pool get_cache_pool(widgets); $keys array_map(function($id) { return widget. . $id; }, $ids); $items $pool-getItems($keys); $widgets array(); foreach ($items as $key $item) { if ($item-isHit()) { $value $item-get(); } else { $value expensive_widget_load($id); $item-set($value); $item-expiresAfter(3600); $pool-saveDeferred($item, true); } $widget[$value-id()] $value; } $pool-commit(); // If no items were deferred this is a no-op. return $widgets; }这个例子浓缩了 PSR-6 最具特色的三个能力getItems()一次性取回多个 Item配合isHit()区分命中/未命中expiresAfter(3600)设置 1 小时 TTL也可用expiresAt()指定绝对过期时间saveDeferred()commit()先把多个 Item 排队为“延迟保存”最后一次性commit()落盘——对应规范中Deferred定义Pool 可能为了利用某些存储引擎的批量写入而延迟持久化但MUST 保证延迟条目最终被持久化且数据不丢失调用commit()时所有未决条目 MUST 全部持久化。文档注释特别提醒若无延迟条目commit()是 no-op。从规范接口看saveDeferred()返回 false 表示“入队失败或尝试 commit 失败”commit()返回 true 表示“全部保存成功或本无未决条目”。4.6 规范之外的扩展示例Taggable/** * This examples reflects functionality that is NOT included in this * specification, but is shown as an example of how such functionality MIGHT * be added by extending implementations. */ interface TaggablePoolInterface extends Psr\Cache\CachePoolInterface { /** * Clears only those items from the pool that have the specified tag. */ clearByTag($tag); } interface TaggableItemInterface extends Psr\Cache\CacheItemInterface { public function setTags(array $tags); } /** * Caches a widget with tags. */ function set_widget(TaggablePoolInterface $pool, Widget $widget) { $key widget. . $widget-id(); $item $pool-getItem($key); $item-setTags($widget-tags()); $item-set($widget); $pool-save($item); }这是对“可扩展且不冲突”目标的直接演示标签tagging虽被列为 Non-Goal但任何实现方都可以通过新增接口并继承 PSR-6 接口来叠加该能力无需修改规范本身。注意示例中Psr\Cache\CachePoolInterface是早期讨论稿的命名正式规范中该接口最终定名为Psr\Cache\CacheItemPoolInterface见 PSR-6-cache.md 接口一节这也说明 meta 文档示例的非规范性质。5. 被否决的替代方案设计取舍全记录Meta 文档最有价值的部分之一是完整记录了三个被否决的方案及其否决理由——这正是 PSR 工作流章程 所要求的Draft 阶段获得的所有知识替代方案、其影响、利弊及选择理由必须总结进 meta 文档以防止已定案的议题再度引发循环讨论。5.1 “弱项”Weak Item方案构造器成为伪接口早期草稿采用更简单的“键值 过期”方式即弱项方案Cache Item 只是一个“带方法的哑数组对象”用户直接实例化它再传给 Pool。它更符合传统直觉Pros但实际上阻止了对 Cache Item 的任何有意义的扩展——Item 的构造函数成为隐式接口的一部分严重限制了可扩展性也剥夺了“让智能逻辑住在缓存条目内部”的可能性。文档记录2013 年 6 月的一次投票中多数参与者明确偏好更健壮、虽不那么传统的“强项”Strong Item/ 仓储方案后者最终被采纳。5.2 “裸值”Naked Value方案无法区分 miss 与 null最早期讨论曾提议完全跳过 Cache Item 概念直接读写原始缓存值。文档指出该方案的致命缺陷无法区分“缓存未命中”与“被选作未命中哨兵的原始值”——例如若get()返回 null无法判断是没有缓存值还是恰好缓存了 null而 null 在许多场景下是合法的缓存值。文档佐证了这一判断的现实依据所调研的健壮缓存实现——尤其是Stash 缓存库和Drupal 自研缓存系统——都在get时返回某种结构化对象以避免 miss 与哨兵值的混淆。基于这些先例经验FIG 判定裸值get方案不可行。5.3 ArrayAccess Pool数组语法不足取曾有建议让 Pool 实现ArrayAccess以支持$pool[key]的数组语法读写。该方案因兴趣有限、灵活性受限只能做带默认控制信息的简单 get/set而被否决同时文档指出若某个具体实现想要这种语法自行添加为附加能力是微不足道的——再次体现了“规范保持底线、扩展留给实现”的设计哲学。6. 接口蓝图与错误处理底线虽然本文主体是 meta 文档但要把设计决策讲透必须对照 PSR-6 正式规范 的接口定义因为 meta 文档的设计词就是对这些接口的论证。规范核心接口如下完整签名见规范文档CacheItemInterfacePsr\Cache命名空间getKey()返回条目 keyget()取回值isHit()为 false 时 MUST 返回 nullisHit()判断本次查找是否命中且 MUST NOT 存在 isHit() 与 get() 之间的竞态条件set($value)设置值返回自身static以支持链式调用expiresAt($expiration)设置绝对过期时间\DateTimeInterface|nullexpiresAfter($time)设置相对过期时间int秒 或\DateInterval|null。CacheItemPoolInterfacegetItem($key)、getItems(array $keys)取回单个/多个 Item未命中也必须返回 Item 对象hasItem($key)存在性检查规范提醒可能因避免取回值而存在竞态建议改用isHit()clear()、deleteItem($key)、deleteItems(array $keys)清理/删除save($item)、saveDeferred($item)、commit()立即持久化 / 延迟持久化 / 提交。异常接口CacheException所有实现方异常 MUST 实现它典型场景如缓存服务器连接失败、凭据无效与InvalidArgumentException任何非法参数 MUST 抛出实现此接口的异常后者继承前者。错误处理底线规范 “Error handling” 一节与 meta 文档“缓存不应成为应用关键路径”的理念一脉相承缓存错误不应导致应用失败实现方MUST NOT 抛出接口定义之外的异常并 SHOULD 捕获底层存储触发的错误/异常、不允许其向外冒泡同时 SHOULD 记录日志或上报管理员删除不存在的 key 或清空空池不构成错误条件。数据面实现方 MUST 支持所有可序列化 PHP 类型字符串、整数、浮点、布尔、null、任意深度的数组、可无损序列化/反序列化的对象且返回的值必须与存入时完全一致含类型——例如存的是(int) 5返回(string) 5即属错误若无法精确返回原值MUST 以缓存未命中响应而非返回损坏数据。Key 约束影响所有缓存操作key 为至少 1 字符的字符串实现方 MUST 支持A-Z、a-z、0-9、_、.组成的 UTF-8 编码、最长 64 字符的 key{}()/\:这组字符被保留给未来扩展实现方 MUST NOT 支持调用方负责自行转义但实现方必须能返回原始未修改的 key 字符串。7. Meta 文档在 PSR 工作流中的制度角色理解这份 meta 文档还要知道它为什么长这样。PSR 工作流章程 规定Draft 阶段所有已获知识替代方案、利弊、选型理由必须汇总进 meta 文档目的是防止已定案议题反复出现——这正是第 4、5 节如此详尽的原因。此外PSR 修订章程 明确规定Errata勘误澄清只能添加到 meta 文档而不是规范本身规范正文最多做最小编辑以指向相关 errata。这解释了为什么第 8 节“Errata”会出现在 meta 文档中也解释了为什么 expiresAt() 的类型澄清放在这里而非 PSR-6-cache.md 中。8. Errata 8.1expiresAt() 的 DateTime 参数处理规范中CacheItemInterface::expiresAt()的$expiration参数在接口里未做类型声明docblock 中标注为\DateTimeInterface。意图是允许传入\DateTime或\DateTimeImmutable。问题在于\DateTimeInterface和\DateTimeImmutable是PHP 5.5 才引入的而规范作者选择不在语法层面强制要求 PHP 5.5。Errata 据此给出硬性规定与实现建议实现方MUST 只接受\DateTimeInterface或兼容类型如\DateTime、\DateTimeImmutable视同该方法已显式声明类型由于模拟“类型检查失败”的行为在不同 PHP 版本间有差异不推荐去模拟原生类型错误取而代之实现方SHOULD 抛出\Psr\Cache\InvalidArgumentException实例。Meta 文档还给出了推荐的强制类型检查参考代码class ExpiresAtInvalidParameterException implements Psr\Cache\InvalidArgumentException {} // ... if (! ( null $expiration || $expiration instanceof \DateTime || $expiration instanceof \DateTimeInterface )) { throw new ExpiresAtInvalidParameterException(sprintf( Argument 1 passed to %s::expiresAt() must be an instance of DateTime or DateTimeImmutable; %s given, get_class($this), is_object($expiration) ? get_class($expiration) : gettype($expiration) )); }这段代码的要点显式允许nullnull 表示“使用默认时长或永久缓存”接受\DateTime与\DateTimeInterface其余一切类型含整型秒数——那只属于expiresAfter()一律抛出实现InvalidArgumentException的异常并在消息中给出实际传入类型的诊断信息。9. Errata 8.2psr/cache 2.0 与 3.0 的类型演进第 8.2 节记录了规范发布后的类型体系升级路线是理解“如何在保持兼容的同时引入强类型”的绝佳案例2.0 版psr/cache包为接口方法补充标量参数类型3.0 版进一步补充返回类型这种分层结构利用 PHP 7.2 的协变covariance支持实现渐进式升级但完整的类型兼容要求 PHP 8.02.0 版同时修正了 Errata 8.1——为expiresAt()的$expiration参数提供了正确的原生类型提示。由于非法输入依然是“致命的不允许情形”FIG 判定这是可接受的小型 BC 破坏以换取正确的原生类型。对实现方Implementing Library的规则想做的事条件添加返回类型返回类型与 3.0 包一致最低 PHP 版本 8.0.0在新主版本添加参数类型参数类型与 2.0 包一致或更宽最低 PHP 版本 8.0.0依赖psr/cache: ^2.0 \|\| ^3.0以排除无类型的 1.0 版规范正文末尾还补充说明2.0 起上述接口已增加参数类型提示3.0 起增加返回类型提示且array|\Traversable引用已替换为iterable。实现方被鼓励而非强制尽早向 3.0 版本迁移。10. PSR-6 与 PSR-16Simple Cache的关系作为背景延伸PSR-6 之后 PHP-FIG 还发布了PSR-16Simple Cache其 Meta 文档 直言“PSR-6 已经解决了这个问题但对最简单的用例来说相当正式和冗长”因此 PSR-16 旨在 PSR-6 接口之上构建一层“标准化简洁层”其 规范文档 也说明 PSR-16 的 Callling Library、TTL、Expiration、Key 等定义直接复制自 PSR-6主要偏差是“缓存未命中返回 null因此无法区分存了 null 与未命中”。一个缓存库可以同时对外暴露两套接口。理解 PSR-6 的“仓储模型”设计正是理解 PSR-16 为何存在、以及何时该用哪一层的先决条件。11. 结语一份标准的设计复盘范本纵观整份 PSR-6 Meta 文档它远不止是规范的附属说明而是一份高质量的设计复盘从问题定义碎片化缓存生态、目标边界基础/中级需求 可扩展机制到核心选型仓储模型而非键值模型、五个实战示例、三个被否决方案的完整记录再到 errata 阶段对 PHP 版本演进的渐进式类型改造。它向我们展示了 PHP-FIG 如何通过“规范 meta”双文档制度既固化标准、又保留决策上下文——这份上下文正是后续实现者、维护者与整个 PHP 社区最宝贵的技术资产。若想深入接口签名细节请继续阅读 PSR-6-cache.md想了解 PSR 从 Draft 到 Accepted 的完整流程可参阅 bylaws/002-psr-workflow.md。赞分享文档开发工具【免费下载链接】fig-standardsStandards either proposed or approved by the Framework Interop Group项目地址https://gitcode.com/gh_mirrors/fi/fig-standards点击查看免费下载相关推荐MPT-7B训练揭秘1T tokens数据如何塑造高效语言模型 MPT 7B训练揭秘1T tokens数据如何塑造高效语言模型 MPT 7B是MosaicML团队开发的一款开源语言模型它在1万亿个tokens的英文PHP-FIG fig-standards 仓库 PSR-2 Meta 文档解读编码风格统一的设计动机、勘误细则与 PSR-12 演进PHP FIG fig standards 仓库 PSR 2 Meta 文档解读编码风格统一的设计动机、勘误细则与 PSR 12 演进 PSR 2 是 PHP文档开发工具推荐文章轻松实现缓存管理 - PHP FIG Simple Cache PSR推荐文章轻松实现缓存管理 PHP FIG Simple Cache PSR 1、项目介绍 PHP FIG Simple Cache PSR 是一个由PHP框架后端缓存抽象上一篇5步解决Playnite游戏库配置导入失败问题下一篇Playnite游戏库管理终极指南从配置灾难到完美恢复创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考