PSR 6 缓存接口 - 说明文档
1. 概要
缓存是提升应用性能的常用手段,为框架中最通用的功能,每个框架也都推出专属的、功能多样的缓存库。这些差别使得开发人员不得不学习多种系统,而很多可能是他们并不需要的功能。此外,缓存库的开发者同样面临着一个窘境,是只支持有限数量的几个框架还是创建一堆庞 大的适配器类。
2. 何必?
这个词很难翻译,找了很多都没有适合的,最后选择了何必? 何必? 这是在问自己,一定要这么做吗?一定要这么规范么?
一个通用的缓存系统接口可以解决这类问题:库和框架的开发人员可以期待缓存系统以它们想要的方式工作,而缓存系统的开发人员只需要实现一组接口而不是各式各样的是适配器
此外,这里介绍的实现是为未来的可扩展性而设计的。它允许内部逻辑不同但 API 兼容的实现,并为未来的扩展( 未来的新 PSR 或特定的实现 ) 提供了一条清晰的道路
优点:
-
一个标准的缓存接口允许独立的类库支持缓存中介数据而不必做任何更改,它们可以简单 ( 可选地 ) 依赖于这个标准接口并利用它而不用担心实现细节
什么叫做中介数据呢?因为缓存的数据是接口传递给它们的,所以叫做中介数据
-
为多个项目开发的通用的可共享的缓存库,即使是在此接口上进行扩展,也可能比十几个单独开发的实现更健壮
缺点:
-
任何接口的标准化都会将未来的创新扼杀为 「 这不是它的责任 not the Way It's Done(tm) 」 "。尽管如此,我们仍认为缓存是一个足够商品化的问题空间,因此在此提供的扩展功能可以缓解任何潜在的停滞风险
怎么理解这种足够商品化...只能说,缓存优化没有最佳答案...探索永无止境
3. 内容
3.1 目标
- 基础或者中级缓存需求的通用接口
- 一个明确的机制允许未来的 PSR 或个别实现来扩展此规范以提供更高级的功能。该机制必须允许存在多个独立的扩展而不会产生冲突
3.2 非目标
- 兼容所有已经存在的缓存框架的构架
- 高级缓存功能,如较少使用场景的名称空间或标记
4. 决策
4.1 选择决策
本规范采用 「 仓储模式 ( repository model ) 」 或 「 数据映射 ( data mapper ) 」 进行缓存,而不是采用更传统的 「 过期 键-值 ( expire-able key-value ) 」 模式。
主要原因是灵活性,一个简单的键/值模型更难以扩展
「 仓储模式 ( repository model ) 」 模式要求使用表示缓存项的 CacheItem
对象和用于缓存数据的给定缓存池对象。
缓存项从缓存池中获取,做一些处理,然后返回它。虽然有时它更冗长一些,但它提供了一种良好的,健壮的,灵活的缓存方式,特别是在缓存比保存和获取字符串更复杂的情况下
大多数方法名是从调查成员项目和其它流行的非成员系统的惯例和方法名称中选择的
- 优点:
-
灵活可扩展
-
允许在实现接口时自由的扩展
-
不隐式地将对象构造函数暴露为伪接口
- 缺点
- 比原生的方法稍微冗长一些
范例
下面显示了一些常见的用例,这些都是非规范性的,只是用于演示一些设计决策
<?php /** * 获取一列可用的小部件列表 * * 在这个用例中,我们假设小部件缓存列表很少被更新,而且是永久性缓存的,除非显示移除过 */ 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(); }
<?php /** * 缓存一列可用的小部件 * * 在这个用例中,我们假设这列小部件是处理过的,我们想要缓存它们,不管它们是否已经在缓存池中 */ function save_widget_list($list) { $pool = get_cache_pool('widgets'); $item = $pool->getItem('widget_list'); $item->set($list); $pool->save($item); }
<?php /** * 移除一列可用小部件的缓存 * * 在这个用例中,我们只是单纯的想要从缓存中移除一列小部件,我们并不关心小部件有没有被缓存 * 提交的动作有点类似于 **不再设置** */ function clear_widget_list() { $pool = get_cache_pool('widgets'); $pool->deleteItems(['widget_list']); }
<?php /** * 移除所有小部件的缓存 * * 在这个用例中,我们想要清空小部件的缓存池 * * 应用程序中的其它缓存池则不受影响 */ function clear_widget_cache() { $pool = get_cache_pool('widgets'); $pool->clear(); }
<?php /** * 加载小部件 * * 我们尝试取回一列小部件,其中的一些小部件被缓存,而另一些则没有 * 当然,这里假定从缓存中加载比从任何非缓存中加载机制都快 * * 在这个用例中,我们假设小部件会被频繁的更改,因此我们只允许它们缓存一个小时 ( 3600 秒 ) * 我们会将新加载的对象给全部缓存起来 * * **注意:** 真正的实现可能需要一个小部件的多重加载操作,但这与此演示无关 */ 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; }
<?php /** * 这个范例中的函数并 **不** 在此规范中,只是用于演示如何扩展此规范 */ interface TaggablePoolInterface extends Psr\Cache\CachePoolInterface { /** * 只移除缓存池中包含指定标签的项 */ clearByTag($tag); } interface TaggableItemInterface extends Psr\Cache\CacheItemInterface { public function setTags(array $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); }
4.2 放弃: "Weak item" 方式
早期的各种草案采用了一个更简单的 「 键值对和过期时间 ( key value with expiration ) 」 的方式,也称为 「 弱条目 ( weak item ) 」的方式。 在这种模式中 ,「 缓存项 ( Cache Item ) 」 实际上是一个哑巴式的 array-with-methods 对象。 用户直接实例化它,然后将其传递给缓存池。 尽管更加简单,也能有效地防止了缓存项的任何有意义的扩展,有效地使缓存项的构造函数成为隐式接口的一部分,但此方法也因此严重缩减了可扩展性,或者使得缓存项变得更加不智能。
在 2013 年 6 月进行的一项民意调查中,大多数参与者明显的偏向不那么传统的 「强条目 ( Strong item ) 」 / 仓储模式 ( repository model ),这种方式也更具前瞻性。
- 优点:
- 更传统的方法
- 缺点:
- 扩展性和灵活性差
4.3 放弃 : "Naked value" 方式
缓存规范的一些早期的建议是跳过缓存项目的概念,只读取/写入要缓存的原始值。 这样比较简单,但有人指出,如果这么做,那么不可能区分缓存未命中和选择了其它值来表示缓存未命中。
比较拗口,不过也很好理解,例如,如果我们选择返回 NULL 值来表示缓存未命中,那么,如果为了让缓存 key 存在而故意赋值为 NULL ,那要怎么区分呢? 对于应用程序来说,这完全不是事,因为可以内部约定就好,但对于类库来说,这是天大的问题。类库最重要的一个指标是盲用性...
也就是说,如果获取缓存时返回 NULL
,则不可能判定缓存是否存在,或者说 NULL
值就是缓存的值,因为在许多情况下,NULL
是合法的缓存值。
我翻译的时候,还没翻译到下面这段,所以自己就写了一个小说明,没想到真对的上
我们回顾了大多数更健壮的缓存实现,特别是 Stash
缓存库和 Drupal
使用的本地缓存系统,在 get
操作中使用某种结构化对象,避免混淆缓存未命中和哨兵 ( sentinel ) 值.
根据之前的经验,FIG 决定在 get
操作时,不可能返回 naked value
raw value 和 naked value ... 有啥区别
4.4 放弃 : 数组访问池 ( ArrayAccess Pool )
标题特难翻译...
一种建议是让池 ( Pool ) 实现 ArrayAccess
,这样可以使用访问数组的语法 get/set 操作访问缓存。
因为兴趣有限,这种形式的灵活性有限 ( 使用默认控制信息的简单获取和设置是可行的 ),且对于添加了附加组件的特定的实现来说太微不足道了,所以,被拒绝了
5. 参与人员
5.1 编撰者
- Larry Garfield
5.2 贡献者
- Paul Dragoonis, PPI Framework (Coordinator)
- Robert Hafner, Stash
6. 表决结果
Acceptance vote on the mailing list
7. 参考文档
注意: 按时间顺序排序
- Survey of existing cache implementations, by @dragoonis
- Strong vs. Weak informal poll, by @Crell
- Implementation details informal poll, by @Crell
8. 勘误表
8.1 在 expiresAt() 函数中处理不正确的 DateTime 值
CacheItemInterface::expiresAt()
方法的 $expiration
参数在接口中是无类型的,但在文档注释中却被规范为 \DateTimeInterface
类型
原本的想法是同时允许 \DateTime
对象或 \DateTimeImmutable
对象。但是,\DateTimeInterface
和 \DateTimeImmutable
是在 PHP 5.5 中被添加的,而撰稿者却没有对规范强加 PHP 5.5 的硬性语法要求
不管则样,类库实现时 必须 只接受 \DateTimeInterface
或兼容类型 ( 例如, \DateTime
和 \DateTimeImmutable
),就好像该方法被明确指定参数类型一样。( 请注意,参数的类型约束可能因开发语言的版本而异 )
在不同的 PHP 版本间模拟一个失败的类型检查时一件很不幸的事情,所以不推荐。
因此,类库实现时 应该 抛出一个 \Psr\Cache\InvalidArgumentException
的异常
建议使用下面的示例代码对 expiresAt()
方法进行参数类型检查
<?php 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) )); }