- 文档
- 开发工具
【免费下载链接】fig-standards
Standards either proposed or approved by the Framework Interop Group
Hypermedia links(超媒体链接)已成为现代 Web 的核心组成,无论是以 HTML 形式还是以 HAL、JSON-LD、Atom 等各类 API 格式呈现。本指南以 PHP-FIG 已接受的 PSR-13 Meta 文档 为主体,结合其规范正文中的完整接口定义,系统讲解 PSR-13 的设计动机、四个核心接口、属性与关系语义,以及 1.1/2.0 版本的类型演进,帮助读者掌握如何用一套与序列化格式无关的通用接口表示、生成和消费超媒体链接。
1. 背景:为什么需要统一的超媒体链接表示
超媒体链接在 Web 中的重要性正在持续上升,它既出现在 HTML 场景中,也广泛存在于各种 API 格式(如 HAL、JSON-LD、Atom)之中。然而,整个行业并不存在一种统一的超媒体格式,也不存在跨格式表示链接的通用方式。这意味着:一个系统如果要把响应中的链接输出到多种"线上格式"(wire format),就必须针对每种格式分别实现一套链接语义。
PSR-13 的目标正是为 PHP 开发者提供一种简单、通用、与序列化格式无关的超媒体链接表示方式。这样一来,一个系统可以先独立地决定"这个响应应该包含哪些链接",再交由序列化层(Serializer)把这些链接对象输出为一种或多种线上格式,两步解耦、互不干扰。正如 Meta 文档 Summary 所述:
This specification aims to provide PHP developers with a simple, common way of representing a hypermedia link independently of the serialization format that is used.
从仓库的 PSR 索引 可以看到,PSR-13(Hypermedia Links)由 Larry Garfield 维护,当前状态为Accepted(已接受),其配套实现由psr/link包提供(规范正文见 accepted/PSR-13-links.md)。
2. 范围界定:做什么与不做什么
Meta 文档第 2 节明确了 PSR-13 的边界:
- 目标(Goals):在不同格式之间抽取并标准化超媒体链接的表示方式。换句话说,规范回答的是"链接在 PHP 对象层面长什么样",而不是"链接在某一种格式里怎么写"。
- 非目标(Non-Goals):规范不试图标准化或偏袒任何一种特定的超媒体序列化格式。HAL、JSON-LD、Atom、HTML 之间的差异依然存在,PSR-13 只负责让它们共享同一套对象模型。
这一边界决定了 PSR-13 的接口非常克制:它只定义"链接是什么、如何读取、如何演化",把"如何渲染"完全交给各个序列化器。
3. 核心设计决策:三个关键问题
Meta 文档第 3 节记录了该规范形成过程中的三个关键设计决策,理解它们等于理解了整个接口骨架的由来。
3.1 为什么没有就地修改方法(mutator methods)?
一个重要的设计考量是:PSR-13 的关键目标对象之一是PSR-7 Response 对象,而 PSR-7 的设计要求 Response 对象不可变(immutable)。其他值对象(value object)实现也大概率需要不可变接口。
另一方面,某些 Link Provider 对象可能根本不是值对象,而是某个领域对象——它能够基于数据库查询结果或其他底层表示即时生成链接。对于这类对象,"可写"的 Provider 定义反而完全不兼容。
因此,PSR-13 把**访问方法(accessor)与可演化方法(evolvable)**拆分成两套独立接口,允许实现者根据自己的用例只实现只读版本或可演化版本。这正是下文LinkInterface与EvolvableLinkInterface双接口并存的根本原因。
3.2 为什么 rel(关系)在一个 Link 对象上是多值的?
不同的超媒体标准对"相同关系的多个链接"处理方式截然不同:有的用"一个链接带多个 rel",有的用"一个 rel 条目下面挂多个链接"。
PSR-13 选择"每个 Link 对象唯一、但允许携带多个 rel",作为最大兼容公约数(most-compatible-denominator):
- 单个
LinkInterface对象可以在某个超媒体格式中被序列化为一个或多个链接条目; - 反过来,"多个 Link 对象共享同一个 URI、各带一个 rel"也是合法的,格式可以按需序列化。
这种双向容忍让 PSR-13 能够适配尽可能多的既有格式。
3.3 为什么需要LinkProviderInterface?
在很多场景下,一组链接会依附于某个其他对象(比如代表 HAL、JSON-LD、Atom 等各种 REST 格式的值对象),而使用方往往只关心其中的链接或链接的子集。Meta 文档举了两个典型用例:
- 从某个对象中提取
next/previous链接,追加到 PSR-7 Response 的Link头中; - 把大量链接表示为
preload关系,让兼容 HTTP/2 的 Web 服务器提前把被引用的资源推送给客户端,为后续请求做准备。
上述场景都与对象的载荷(payload)或编码无关。通过提供统一的链接访问接口,PSR-13 让链接的通用处理成为可能,无论产生链接的是值对象还是领域对象。
4. 链接的构成:URI、关系与属性
在进入接口代码之前,先明确 PSR-13 规范正文(accepted/PSR-13-links.md 第 1 节)对链接构成的定义:
- 一个超媒体链接至少包含:URI(被引用目标资源的地址)与关系(目标资源与源资源的关联方式);
- 链接还可以有零个或多个额外属性,但属性缺乏统一注册表,合法性依赖具体上下文和序列化格式,因此规范不试图标准化它们。常见的属性包括
hreflang、title、type。
规范还定义了两种实现角色:
- Implementing Object:实现了本规范任一接口的对象;
- Serializer:接收一个或多个 Link 对象并输出某种格式序列化表示的库或系统。
4.1 属性(Attributes)序列化规则
- 序列化器在格式要求时可以省略属性,但应当尽可能编码所有提供的属性以支持用户扩展;
- 某些属性(如
hreflang)在上下文中可能多次出现,因此属性值可以是数组,序列化器可按格式特点编码(空格分隔、逗号分隔等);若某格式不允许多值,序列化器必须取第一个值并忽略其余; - 属性值为布尔
true时,序列化器可以在格式支持的情况下使用简写(如 HTML 的"无值属性");该规则只适用于布尔true,不适用于 PHP 中其他"truthy"值(如整数1); - 属性值为布尔
false时,序列化器应当完全省略该属性(除非省略会改变语义);该规则同样只适用于布尔false,不适用于其他"falsey"值(如整数0)。
4.2 关系(Relationships)
链接关系以字符串表示,分两类:
- 公开关系:使用简单关键字(keyword),应当匹配 IANA Link Relations Registry 中的条目;可选地也可以使用 microformats.org 的关系列表,但后者并非在所有上下文都有效;
- 私有关系:凡未在公共注册表中定义的关系,视为应用或用例私有的,必须使用绝对 URI 表示。
4.3 链接模板(Link Templates)
RFC 6570 定义了 URI 模板格式——一种期望由客户端工具填充值的 URI 模式。部分超媒体格式支持模板化链接,部分不支持并有特殊的标记方式。规范要求:不支持 URI 模板的序列化器必须忽略所遇到的模板化链接。
5. 四个核心接口:从只读到可演化
PSR-13 规范正文(第 3 节)完整给出了四个接口的 PHP 定义,这也是 Meta 文档中"只读/可演化拆分"设计决策的具体落地。以下代码可直接用于理解或作为实现的参考。
5.1Psr\Link\LinkInterface:只读链接对象
<?php namespace Psr\Link; /** * A readable link object. */ interface LinkInterface { /** * Returns the target of the link. * * The target link must be one of: * - An absolute URI, as defined by RFC 5988. * - A relative URI, as defined by RFC 5988. The base of the relative link * is assumed to be known based on context by the client. * - A URI template as defined by RFC 6570. * * If a URI template is returned, isTemplated() MUST return True. * * @return string */ public function getHref(); /** * Returns whether or not this is a templated link. * * @return bool * True if this link object is templated, False otherwise. */ public function isTemplated(); /** * Returns the relationship type(s) of the link. * * This method returns 0 or more relationship types for a link, expressed * as an array of strings. * * @return string[] */ public function getRels(); /** * Returns a list of attributes that describe the target URI. * * @return array * A key-value list of attributes, where the key is a string and the value * is either a PHP primitive or an array of PHP strings. If no values are * found an empty array MUST be returned. */ public function getAttributes(); }要点:
getHref()允许返回绝对 URI(RFC 5988)、相对 URI(基准由客户端按上下文推定)或 RFC 6570 URI 模板;若返回模板,isTemplated()必须返回true;getRels()返回零个或多个关系字符串数组(对应上文"多值 rel"决策);getAttributes()返回键为字符串、值为 PHP 基本类型或字符串数组的键值列表,无属性时必须返回空数组。
5.2Psr\Link\EvolvableLinkInterface:可演化链接值对象
<?php namespace Psr\Link; /** * An evolvable link value object. */ interface EvolvableLinkInterface extends LinkInterface { /** * Returns an instance with the specified href. * * @param string $href * The href value to include. It must be one of: * - An absolute URI, as defined by RFC 5988. * - A relative URI, as defined by RFC 5988. The base of the relative link * is assumed to be known based on context by the client. * - A URI template as defined by RFC 6570. * - An object implementing __toString() that produces one of the above * values. * * An implementing library SHOULD evaluate a passed object to a string * immediately rather than waiting for it to be returned later. * * @return static */ public function withHref($href); /** * Returns an instance with the specified relationship included. * * If the specified rel is already present, this method MUST return * normally without errors, but without adding the rel a second time. * * @param string $rel * The relationship value to add. * @return static */ public function withRel($rel); /** * Returns an instance with the specified relationship excluded. * * If the specified rel is already not present, this method MUST return * normally without errors. * * @param string $rel * The relationship value to exclude. * @return static */ public function withoutRel($rel); /** * Returns an instance with the specified attribute added. * * If the specified attribute is already present, it will be overwritten * with the new value. * * @param string $attribute * The attribute to include. * @param string $value * The value of the attribute to set. * @return static */ public function withAttribute($attribute, $value); /** * Returns an instance with the specified attribute excluded. * * If the specified attribute is not present, this method MUST return * normally without errors. * * @param string $attribute * The attribute to remove. * @return static */ public function withoutAttribute($attribute); }这一接口完全复刻了 PSR-7 值对象的"返回新实例"模式:所有with*方法都返回static,即"与原始对象相同但只做了一处修改"的新对象。得益于 PHP 的写时复制(copy-on-write)行为,这种演化方式依然具备优秀的 CPU 与内存效率。注意一个细节:withHref()接受实现__toString()的对象,并建议实现库立即将其求值为字符串,而不是延迟到返回之后。
没有针对模板化值(templated)的演化方法:因为链接的模板化状态完全取决于 href 值本身——它不能被独立设置,只能由"href 是否为 RFC 6570 URI 模板"推导得出。
5.3Psr\Link\LinkProviderInterface:只读链接提供者
<?php namespace Psr\Link; /** * A link provider object. */ interface LinkProviderInterface { /** * Returns an iterable of LinkInterface objects. * * The iterable may be an array or any PHP \Traversable object. If no links * are available, an empty array or \Traversable MUST be returned. * * @return LinkInterface[]|\Traversable */ public function getLinks(); /** * Returns an iterable of LinkInterface objects that have a specific relationship. * * The iterable may be an array or any PHP \Traversable object. If no links * with that relationship are available, an empty array or \Traversable MUST be returned. * * @return LinkInterface[]|\Traversable */ public function getLinksByRel($rel); }getLinks()返回数组或任意\Traversable对象,无链接时必须返回空集合;getLinksByRel($rel)按关系过滤。这正是 Meta 文档 3.3 节所述"从各种值对象/领域对象中统一抽取链接"的落地接口。
5.4Psr\Link\EvolvableLinkProviderInterface:可演化链接提供者
<?php namespace Psr\Link; /** * An evolvable link provider value object. */ interface EvolvableLinkProviderInterface extends LinkProviderInterface { /** * Returns an instance with the specified link included. * * If the specified link is already present, this method MUST return normally * without errors. The link is present if $link is === identical to a link * object already in the collection. * * @param LinkInterface $link * A link object that should be included in this collection. * @return static */ public function withLink(LinkInterface $link); /** * Returns an instance with the specified link removed. * * If the specified link is not present, this method MUST return normally * without errors. The link is present if $link is === identical to a link * object already in the collection. * * @param LinkInterface $link * The link to remove. * @return static */ public function withoutLink(LinkInterface $link); }注意与普通 Provider 的差异:可演化 Provider 的withLink()/withoutLink()同样返回static新实例。原因在规范正文第 1.5 节写得很清楚——像 PSR-7 Response 这类对象按设计不可变,就地添加链接的方法与之根本不兼容,因此唯一的方法必须是"返回一个与原来相同但多了一个 Link 的新对象"。链接是否已存在、是否被移除,都以===全等比较为准。
6. 演化模型:为什么"不可变 + with*"是合理选择
Meta 文档与规范正文共同勾勒出一套完整的设计哲学:
- 链接对象在大多数情况下是值对象,允许它们像 PSR-7 值对象一样演化是一种有用的能力,因此引入
EvolvableLinkInterface; - 链接提供者分两类:有的需要能往里追加链接,有的天生只读(链接运行时从其他数据源推导),因此可修改的 Provider 是可选实现的次级接口;
- 模板化状态不可独立设置,只能由 href 推导,因此接口中不存在
withTemplated()之类的方法。
这套"只读接口 + 可演化接口"的双层结构,与 PSR-7 一脉相承(PSR-7 的规范背景可参见 accepted/PSR-7-http-message-meta.md)。事实上,这种"事件/对象可演化"的思路还被后续标准继承——PSR-14 Event Dispatcher Meta 文档 就明确要求 Event 可演化(不可变但提供with*()方法,如同 PSR-7 与 PSR-13)。
7. 版本演进与类型系统(Errata)
Meta 文档第 7 节记录了规范发布后的两处重要修订,对实现者至关重要。
7.1 类型补充(Type additions)
psr/link包的演进刻意采用了"渐进式升级"策略:
- 1.1 版本:加入标量参数类型(scalar parameter types);
- 2.0 版本:加入返回类型,并将
array|\Traversable的引用替换为iterable; - 该结构利用 PHP 7.2 的协变(covariance)支持实现渐进升级,但要获得完整的类型兼容则需要PHP 8.0。
实现者(implementers)的约束如下:
- 实现者可以自行在其包中加入返回类型,前提是:返回类型与 2.0 包一致,且实现声明最低 PHP 版本为 8.0.0 或更高;
- 实现者可以在新的大版本中加入参数类型(可与返回类型同时加入,也可随后续版本加入),前提是:参数类型与 1.1 包一致、最低 PHP 版本为 8.0.0 或更高,并且依赖声明为
"psr/link": "^1.1 || ^2.0"以排除无类型的 1.0 版本; - 鼓励(但不强制)实现者尽早向 2.0 版本过渡。
7.2 属性类型处理(Attribute type handling)
原始规范存在一处不一致:规范正文第 1.2 节说明传给EvolvableLinkInterface::withAttribute()的属性值可以是多种类型(其中部分允许特殊处理,如布尔值或数组),但该方法的 docblock 却错误地把$value参数限定为字符串。
后续版本已修正接口,允许$value为以下类型:
string|\Stringable|int|float|bool|array配套规则:
- 实现者应当将
Stringable对象视同string参数处理; - 实现者可以为特定序列化格式按类型感知的方式序列化
int、float或bool; - 其他对象类型与资源(resource)仍被禁止;
- 以相同
$name多次调用withAttribute()必须覆盖先前的值(规范第 1.2 节已有此要求);要为某属性提供多个值,请传入包含所需值的array; - 第 1.2 节其余的所有准则与要求保持不变。
8. 实战场景串联:从领域对象到 HTTP 响应
把以上内容串起来,一个典型的 PSR-13 使用流如下:
- 领域层产出链接:某个代表 HAL/JSON-LD/Atom 的值对象实现
LinkProviderInterface,基于底层数据即时返回链接集合(getLinks()、getLinksByRel('next')); - 通用处理:中间件不关心该对象的载荷与编码,只通过统一接口抽取链接——这正是 Meta 文档 3.3 节强调的"通用处理";
- 落线上格式:序列化器读取
LinkInterface的getHref()、getRels()、getAttributes()、isTemplated(),按目标格式输出(HTML<link>、HTTPLink头、HAL_links、JSON-LD 等); - HTTP/2 预加载:需要"推送"的资源以
preload关系表达,兼容 HTTP/2 的服务器据此提前流式传输被引用资源,为后续请求做准备; - 可演化路径:若目标对象(如 PSR-7 Response)不可变,则通过
EvolvableLinkProviderInterface::withLink()返回新实例,把额外链接追加到 Response 的Link头中。
9. 参与背景与进一步阅读
根据 Meta 文档第 4 节,PSR-13 的 Editor 为 Larry Garfield,Sponsors 为 Matthew Weier O'Phinney(coordinator)与 Marc Alexander,Contributor 为 Evert Pot。该标准已列入 PSR 索引 的 Accepted 状态。
若需继续深入,仓库内可参考的资料包括:
- 完整接口定义与规范细节:accepted/PSR-13-links.md;
- 设计决策与修订记录(本文主体):accepted/PSR-13-links-meta.md;
- 与 PSR-13 交互紧密的 PSR-7 背景:accepted/PSR-7-http-message-meta.md;
- 继承其"可演化值对象"思路的 PSR-14:accepted/PSR-14-event-dispatcher-meta.md。
小结:PSR-13 用四个接口、约十个方法,为 PHP 世界确立了一套与序列化格式解耦的超媒体链接对象模型。它不定义任何格式,只定义"链接是什么、怎么读、怎么演化";它不依赖可变对象,而是借助 PSR-7 式的不可变with*()模式保持高效与兼容。理解它的三个核心设计决策——只读/可演化拆分、多值 rel、Provider 抽象——也就理解了整个标准的骨架。
- 文档
- 开发工具
【免费下载链接】fig-standards
Standards either proposed or approved by the Framework Interop Group
相关推荐
PSR-13 超媒体链接接口(Hypermedia Links)实战指南:用 psr/link 统一表示与序列化超媒体链接
PSR 13 超媒体链接接口(Hypermedia Links)实战指南:用 psr/link 统一表示与序列化超媒体链接 PSR 13(Hypermedia
文档开发工具React-Tween-State缓动函数完全解析:从easeInQuad到easeInOutBounce的30+种动画曲线
React Tween State缓动函数完全解析:从easeInQuad到easeInOutBounce的30+种动画曲线 React Tween State
前端PSR-13权威指南:构建PHP标准化HTTP链接的完整实践方案
PSR 13权威指南:构建PHP标准化HTTP链接的完整实践方案 引言:终结PHP链接管理的碎片化困境 你是否正在为不同PHP框架间的链接处理兼容性问题而头疼?
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考