Yii 2 REST API 版本化实战指南:主版本模块隔离与 Accept 头次版本协商
【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2
本指南以 Yii 2 官方文档《REST 版本化》(对应仓库 docs/guide-ru/rest-versioning.md,英文原版见 docs/guide/rest-versioning.md)为核心骨架,结合框架源码展开。REST API 面向不受你控制的客户端,因此版本化是保障向后兼容(Backward Compatibility,简称 BC)的必修课。读完本文,你将掌握 Yii 2 推荐的双层版本化策略:用独立模块承载每个主版本(如v1、v2),用Accept请求头协商次版本,并学会如何在控制器、资源模型与序列化器中读取版本信息编写条件逻辑。
为什么 API 必须版本化
Yii 官方文档开宗明义地指出:好的 API 必须是版本化的——新功能与变更应当通过新的 API 版本引入,而不是在同一个版本上不断改动。原因在于:
- Web 应用的后端与前端代码都在你的掌控之中,可以随时同步升级;
- 而 API 的消费方是你无法控制的客户端(第三方应用、移动端、外部集成方),它们可能长期停留在旧版本上。
因此,API 的向后兼容性应当尽可能保持。如果必须做出破坏兼容性的变更,正确做法是:把它放进一个新版本的 API中并提升版本号。既有客户端可以继续使用旧版本,新客户端则切换到新版本获取新能力。设计版本号时可以参考业界通行的语义化版本(Semantic Versioning,即 Major.Minor.Patch 三段式)思路来规划主版本、次版本与补丁版本的语义边界。
两种主流版本化方式的对比
文档梳理了业界最常见的两种 API 版本化实现方式,它们各有拥趸,也各有取舍。
方式一:URL 路径内嵌版本号
将版本号直接嵌入调用 URL,例如:
https://example.com/v1/users即请求 API 版本 1 的/users端点。这种方式直观、易调试、便于缓存与日志分析,是历史最悠久也最普及的做法。
方式二:HTTP 请求头携带版本号
把版本号放进 HTTP 请求头,通常是Accept头,常见有两种写法:
// 作为参数传递 Accept: application/json; version=v1 // 作为供应商自定义内容类型(vendor content type) Accept: application/vnd.company.myapp-v1+json第一种通过Accept头的参数version=v1声明版本;第二种则把版本号编入 MIME 类型,形如application/vnd.<厂商>.<应用>-v<版本>+<格式>,由 API 供应商自定义。这种方式保持了 URL 的纯净,但调试与缓存配置相对复杂。
两种方式都有各自的优缺点,社区对此争论已久。Yii 官方文档给出的结论是:不要二选一,而是取二者之长,采用一种混合策略。
Yii 2 推荐的混合版本化策略
Yii 2 官方推荐的实践是把两种方式组合为两层:
- 主版本(Major)放进 URL,通过模块隔离:将每个主版本的 API 实现放进一个独立模块中,模块 ID 就是主版本号(例如
v1、v2)。这样 API 的 URL 天然包含主版本号。 - 次版本(Minor)放进
Accept头,通过条件代码响应:在每个主版本模块内部,用Accept请求头确定次版本号,并针对不同次版本编写条件逻辑。
这个分层思路的关键在于:主版本的隔离是物理级的(独立目录、独立类),因为主版本之间允许破坏性变更;而次版本必须保持 BC,因此差异往往只是一些字段或行为上的细微条件判断,用Accept头协商即可,不需要复制整套代码。
按主版本组织代码:模块隔离的目录结构
针对每个服务主版本的模块,模块内应包含服务于该版本的资源类(Resource)与控制器类。为了更好的职责分离,文档建议维护一套公共的基类,然后在每个版本模块内对其子类化,在子类中实现该版本特有的具体代码,例如覆写Model::fields()来定制输出字段。
推荐的代码组织方式如下:
api/ common/ controllers/ UserController.php PostController.php models/ User.php Post.php modules/ v1/ controllers/ UserController.php PostController.php models/ User.php Post.php Module.php v2/ controllers/ UserController.php PostController.php models/ User.php Post.php Module.php要点解读:
common/存放跨版本共享的基类(如UserController、User的基础实现);modules/v1/与modules/v2/分别存放两个主版本的控制器与资源模型子类;- 每个版本模块拥有自己的
Module.php入口类; - 由于主版本之间代码物理隔离,v1 的破坏性改动不会影响 v2;同时通过公共基类仍能跨模块复用逻辑。
关于fields()方法,它是 Yii 资源模型输出字段的开关:framework/base/ArrayableTrait.php(framework/base/ArrayableTrait.php)中默认实现返回所有公共对象成员变量,而framework/base/Model.php(framework/base/Model.php)同样声明了该方法。在版本子类中覆写它,即可让 v2 输出比 v1 更多的字段或改名后的字段,而不影响 v1 的既有输出——这正是版本化资源模型的典型用法。
应用配置:注册版本模块与 REST 路由规则
有了目录结构,还需要在应用配置中注册模块并配置urlManager的路由规则,才能让https://example.com/v1/users这类 URL 真正路由到对应模块。文档给出的完整配置如下:
return [ 'modules' => [ 'v1' => [ 'class' => 'app\modules\v1\Module', ], 'v2' => [ 'class' => 'app\modules\v2\Module', ], ], 'components' => [ 'urlManager' => [ 'enablePrettyUrl' => true, 'enableStrictParsing' => true, 'showScriptName' => false, 'rules' => [ ['class' => 'yii\rest\UrlRule', 'controller' => ['v1/user', 'v1/post']], ['class' => 'yii\rest\UrlRule', 'controller' => ['v2/user', 'v2/post']], ], ], ], ];关键点拆解:
modules段:声明v1、v2两个模块,class指向各自的Module类,模块 ID 即 URL 中的主版本段。enablePrettyUrl与enableStrictParsing:启用美化 URL 与严格解析,保证/v1/users形式的路由能被正确解析而不是落到index.php查询串。yii\rest\UrlRule:这是 REST 路由的核心。规则中的controller数组['v1/user', 'v1/post']中,控制器 ID 以模块 ID 为前缀(v1/),这正是framework/rest/UrlRule.php(framework/rest/UrlRule.php)所要求的写法——其文档注释明确说明:控制器位于模块内时,ID 必须以模块 ID 作为前缀。
UrlRule 底层机制:自动生成整组 REST 路由
从源码看,UrlRule继承自CompositeUrlRule,并内置了一套 REST 端点模式($patterns属性,见 framework/rest/UrlRule.php):
| HTTP 动词 | 模式 | 路由动作 |
|---|---|---|
PUT, PATCH | {id} | update(更新) |
DELETE | {id} | delete(删除) |
GET, HEAD | {id} | view(查看详情) |
POST | (无) | create(创建) |
GET, HEAD | (无) | index(列表) |
| 任意 | {id} | options(预检) |
| 任意 | (无) | options(预检) |
同时它还具备两项重要行为:
- 自动复数化:
$pluralize默认true,init()中通过Inflector::pluralize()把user变为users、post变为posts(见 framework/rest/UrlRule.php),所以 URL 呈现为复数名词; - 按动词拆分规则:
createRule()会解析模式中的 HTTP 动词前缀,为每个动作生成独立的yii\web\UrlRule实例并绑定对应的路由(如v1/user/index)。
因此,上述配置最终产生的效果正如文档所言:
https://example.com/v1/users返回版本 1 的用户列表;https://example.com/v2/users返回版本 2 的用户列表;- 对应地,
POST /v1/users创建、GET /v1/users/123查看详情等 REST 语义端点也一并生效。
得益于模块机制,不同主版本的代码得以良好隔离;同时通过公共基类与其他共享类,模块间依然可以复用代码。仓库中的单元测试 tests/framework/rest/UrlRuleTest.php 对 UrlRule 的路由生成与匹配行为有系统性覆盖,可作为深入理解其行为的参考。
次版本处理:ContentNegotiator 与 acceptParams
主版本交给模块隔离后,次版本的处理依赖 Yii 的内容协商(Content Negotiation)能力——即yii\filters\ContentNegotiator行为(源码见 framework/filters/ContentNegotiator.php)。它有两种挂载方式:作为引导组件(bootstrap)作用于整个应用,或作为行为(behavior)挂在控制器/模块上。在 REST 场景中,yii\rest\Controller已经在behaviors()里默认配置了contentNegotiator(见 framework/rest/Controller.php),支持application/json与application/xml两种格式。
ContentNegotiator在判定支持的响应格式时会顺带解析Accept头中的参数,并把它们写入响应对象的属性。相关属性定义在 framework/web/Response.php:
$acceptMimeType:从请求Accept头选中的 MIME 类型;$acceptParams:与该 MIME 类型关联的参数名值对数组,例如['q' => 1, 'version' => '1.0']。
结合源码中的negotiateContentType()(framework/filters/ContentNegotiator.php)可以还原完整流程:
- 若配置了
formatParam(默认_format)且请求携带该 GET 参数,则直接以该参数决定格式; - 否则遍历
$request->getAcceptableContentTypes(),在formats中查找匹配的 MIME 类型; - 命中后设置
$response->format、$response->acceptMimeType = $type,并把解析出的参数(如version=v1)赋给$response->acceptParams。
于是,文档中的示例得到了源码层面的印证:
如果请求携带
Accept: application/json; version=v1,内容协商完成后,yii\web\Response::acceptParams将包含['version' => 'v1']。
在业务代码中消费版本信息
拿到acceptParams后,就可以在动作(action)、资源类(resource class)、序列化器(serializer)等位置编写条件代码。例如在控制器动作中:
use Yii; public function actionIndex() { $params = Yii::$app->response->acceptParams; $version = $params['version'] ?? 'v1'; // 针对不同次版本返回不同字段或行为 // ... }若需要定制响应序列化方式,可以覆写yii\rest\Serializer(framework/rest/Serializer.php)的serialize(),在其中读取acceptParams按版本输出不同的数组结构;资源模型层面则可以像上文所述通过覆写fields()实现字段差异。这三个落点(动作、资源类、序列化器)也正是文档明确列举的条件代码写入位置。
版本检查的度:何时该开新的主版本
文档最后给出了一条重要的工程经验:次版本按定义必须保持向后兼容,因此期望你的代码中版本号检查不会太多。如果发现代码里充斥着大量版本判断分支,那么大概率意味着这些变更实际上已经破坏了 BC——此时正确的做法不是继续在acceptParams里堆条件,而是开启一个新的主版本模块。
这条建议背后是成本考量:次版本协商只需要写少量条件代码,维护成本低;但条件分支过多会让代码难以阅读和维护。主版本模块隔离虽然成本更高(需要复制/子类化控制器与资源类),却换来了清晰的边界与绝对的兼容保障。在"少量条件判断"与"新开主版本"之间,文档给出的取舍标准就是:检查过多,就该升级主版本了。
延伸阅读
- REST 路由规则详解:docs/guide/rest-routing.md(俄文版 docs/guide-ru/rest-routing.md)
- REST 控制器与动作体系:docs/guide/rest-controllers.md
- REST 资源与字段定制:docs/guide/rest-resources.md
- 响应格式协商与序列化:docs/guide/rest-response-formatting.md
- 核心实现:
UrlRule(framework/rest/UrlRule.php)、ContentNegotiator(framework/filters/ContentNegotiator.php)、Response::acceptParams(framework/web/Response.php)
【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考