
Yii 2 REST API 版本控制实战指南模块化主版本与 Accept 头次版本策略【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2本文是 Yii 2 框架 RESTful API 版本控制Versioning的完整实战指南基于官方指南 docs/guide-pl/rest-versioning.md与 英文版 内容一致展开并结合框架源码深入解析底层实现。文章将讲解 API 版本化的必要性、URL 路径与Accept头两种主流方案的取舍、Yii 2 推荐的主版本走模块 次版本走内容协商混合策略并给出完整的目录结构、应用配置、yii\rest\UrlRule路由规则与yii\filters\ContentNegotiator内容协商的源码级原理与可运行的代码示例。读完本文你将掌握如何为现有 Yii 2 应用平稳地引入多版本 REST API并保持向后兼容。为什么 API 必须版本化一个良好的 API 是有版本的新的变更与新功能应当在新版本 API 中实现而不是持续修改同一个版本。这一点与 Web 应用有本质区别——Web 应用的客户端与服务器端代码都在你的完全掌控之下而 API 注定会被你无法控制的第三方客户端消费。因此只要可能就应该维持 API 的向后兼容性Backward Compatibility简称 BC。如果必须引入一个可能破坏 BC 的变更正确做法是在新版本 API 中引入该变更提升版本号现有客户端继续使用旧的、可正常工作的 API 版本新的或升级后的客户端使用新版本 API 获取新功能。关于版本号的命名设计可参考 语义化版本控制Semantic Versioning 的约定主版本号.次版本号.修订号其中主版本号意味着不兼容的 API 变更。两种主流的版本化实现方式实践中API 版本化主要有两种常见实现方式各有拥趸与争论。方式一版本号嵌入 URL 路径最普遍的做法是把版本号直接写进 API 的 URL 中。例如https://example.com/v1/users即表示 API 版本 1 的/users端点。其优点是直观、易于调试、便于按版本做访问控制与日志分析缺点是一旦 URL 中出现版本号客户端代码与服务器路由都变得不那么干净且难以在共享同一 URL 的情况下演进。方式二版本号放入 HTTP 请求头近年来逐渐流行的方法是使用 HTTP 请求头携带版本号通常通过Accept头实现// 通过参数方式 Accept: application/json; versionv1 // 通过供应商内容类型vendor content type方式 Accept: application/vnd.company.myapp-v1json第一种写法在 MIME 类型后用;追加versionv1参数第二种写法把版本号嵌入自定义的 vendor MIME 类型名中。其优点是 URL 保持纯净、同一资源标识符可以对应多个版本缺点是对客户端要求更高且难以在浏览器地址栏中直接验证。Yii 2 推荐的混合策略两种方法各有优缺点业界争论不休。Yii 2 官方给出了一套将二者结合的实用策略这也是本文的核心每个主版本major version的实现放入一个独立模块模块 ID 即主版本号例如v1、v2。相应地API 的 URL 中会自然包含主版本号。在每一个主版本内部即对应的模块内使用AcceptHTTP 请求头来确定次版本号minor version并编写条件代码来响应不同的次版本。这套策略的精髓在于主版本决定不兼容的分水岭用 URL 隔离次版本决定兼容的增量演进用请求头协商两者各司其职。项目目录结构按主版本隔离按公共基类复用每个承载主版本的模块都应包含服务于该版本的资源类resource与控制器controller类。为了更清晰地划分代码职责你可以把一组公共的基类资源与控制器放在一起然后在各个版本模块中继承它们子类中再实现具体的版本差异代码例如重写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从源码结构可以推断这套设计的两个收益api/common/存放跨版本共享的基类如基础User模型、基础UserController各版本模块中的类通过继承复用它们避免重复代码v1/与v2/模块完全隔离可以独立演进——即使v2的User模型与v1有本质差异如字段重命名、关联关系变更也不会互相干扰。版本模块自身的Module.php继承自 framework/base/Module.php框架在其中提供了controllerNamespace控制器命名空间用于自动映射模块内控制器与defaultRoute默认路由等属性模块化路由得以自动工作。应用配置注册版本模块与 REST 路由规则应用配置如config/main.php示例如下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各版本模块类注册两个主版本模块模块 ID 即 URL 中的版本段urlManager.enablePrettyUrltrue启用美化 URL去掉入口脚本参数urlManager.enableStrictParsingtrue严格解析URL 必须匹配已声明的规则否则 404urlManager.showScriptNamefalse生成的 URL 中不显示入口脚本名如index.phprules[].controller[v1/user, v1/post]等控制器 ID 以模块 ID 为前缀yii\rest\UrlRule会为每个控制器生成全套 REST 路由上述代码的运行效果是https://example.com/v1/users返回版本 1 的用户列表而https://example.com/v2/users返回版本 2 的用户列表。得益于模块机制不同主版本的代码可以很好地相互隔离同时通过公共基类与其他共享资源跨模块的代码复用仍然是可能的。源码级解析yii\rest\UrlRule 如何生成版本化路由配置中使用的 framework/rest/UrlRule.php 是 Yii 2 REST 路由的核心类理解它有助于把握版本化路由的细节1.controller属性必须带模块前缀。源码注释明确指出如果控制器位于模块内控制器 ID 应以模块 ID 作为前缀。因此配置中写的是v1/user、v1/post而不是user、post——否则 URL 中不会包含版本段。2. 自动复数化 URL 名称。public $pluralize trueframework/rest/UrlRule.php在init()中通过Inflector::pluralize()将控制器 ID 转为复数形式于是user出现在 URL 中就是users。3. 一套规则生成全套 REST 端点。patterns属性framework/rest/UrlRule.php定义了标准 REST 动作映射PUT,PATCH {id} update DELETE {id} delete GET,HEAD {id} view POST create GET,HEAD index {id} options optionscreateRules()会为每个控制器、每个 pattern 拼出prefix/urlName前缀并映射到controller/action路由parseRequest()中会先检查路径前缀是否匹配再逐个尝试内部规则。因此一个[class yii\rest\UrlRule, controller [v1/user, v1/post]]声明即可为两个控制器生成GET v1/users、POST v1/users、GET v1/users/123、DELETE v1/users/123等一整套 RESTful 端点。4. 可通过only/except/extraPatterns裁剪与扩展。例如except [delete]可以禁用某个动作的路由extraPatterns可添加自定义模式如POST search search。次版本处理ContentNegotiator 内容协商主版本用模块 URL 解决后**次版本minor version**通过内容协商content negotiation机制处理。其核心是 framework/filters/ContentNegotiator.php 提供的contentNegotiator行为当它确定支持哪种内容类型后会设置 framework/web/Response.php 的acceptParams属性。ContentNegotiator同时支持两种用途源码注释明确说明作为引导组件bootstrapping component在应用配置的bootstrap中声明对整个应用生效作为动作过滤器action filter挂在某个控制器或模块的behaviors()中只对相应控制器/模块甚至通过only/except限定到特定动作生效。例如在版本模块基类控制器中挂载use yii\web\Response; use yii\filters\ContentNegotiator; public function behaviors() { return [ [ class ContentNegotiator::class, formats [ application/json Response::FORMAT_JSON, application/xml Response::FORMAT_XML, ], ], ]; }acceptParams 的产生过程举例说明如果请求携带 HTTP 头Accept: application/json; versionv1经过内容协商后yii\web\Response::acceptParams将包含值[version v1]。其底层流程negotiate()→negotiateContentType()见 framework/filters/ContentNegotiator.php如下若配置了formatParam默认_format且请求携带该 GET 参数则优先按 GET 参数确定格式参数为数组时抛出BadRequestHttpException该行为有测试覆盖见 tests/framework/filters/ContentNegotiatorTest.php。否则读取请求的Accept头通过Request::getAcceptableContentTypes()遍历客户端可接受的内容类型与formats中声明的 MIME 类型匹配命中时设置Response::format、Response::acceptMimeType为命中的 MIME 类型并将Accept头中携带的参数如versionv1原样存入Response::acceptParams全部不匹配且客户端没有通配*/*时抛出NotAcceptableHttpExceptionHTTP 406。当formats声明了多种格式时negotiate()会自动为响应添加Vary: Accept头——这告诉 HTTP 缓存层响应内容取决于Accept头对版本化 API 的缓存正确性至关重要。该行为同样有测试验证tests/framework/filters/ContentNegotiatorTest.php。测试 tests/framework/filters/ContentNegotiatorTest.php 还演示了带版本参数的协商setAcceptableContentTypes([application/json [q 1, version 1.0]])后调用negotiate()Response::format被设为json。Accept 头的解析原理Accept: application/json; versionv1之所以能被解析出versionv1参数依赖于 framework/web/Request.php 的parseAcceptHeader()实现按,拆分多个可接受类型每个类型再按;拆分参数keyvalue形式的参数进入参数表其中q被解析为浮点权重用于偏好排序其余普通段作为附加参数收集。因此Accept: application/json; versionv1; q0.9会被解析为[application/json [q 0.9, version v1]]最终version通过acceptParams暴露给业务代码。基于 acceptParams 编写版本条件代码基于acceptParams中的版本信息你可以在**动作actions、资源类resource classes、序列化器serializers**等位置编写条件代码以提供相应的功能。一个典型例子让同一模型在不同次版本中暴露不同字段。use yii\db\ActiveRecord; class User extends ActiveRecord { public function fields() { $fields parent::fields(); $acceptParams Yii::$app-response-acceptParams; $version $acceptParams[version] ?? v1; if ($version v2) { // v2 新增字段 $fields[profile] profile; } return $fields; } }也可以在控制器动作中按版本分支逻辑public function actionIndex() { $params Yii::$app-response-acceptParams; $query User::find(); if (($params[version] ?? ) v2) { // 仅 v2 支持的过滤或联表逻辑 $query-with(profile); } return $query-all(); }注意事项与设计原则次版本检查点不宜过多。由于次版本按定义必须维持向后兼容代码中不应出现太多检查版本号的位置——这往往意味着 API 设计在快速漂移。如果条件分支变得难以维护很可能就是需要创建下一个主版本v3的信号。模块 ID 即版本号尽量保持语义化版本中的主版本号一致如v1、v2URL 结构即版本声明。内容协商结果与缓存正确性相关声明多种格式时框架会自动输出Vary: Accept但若你在动作层进一步按acceptParams[version]输出不同内容也应注意缓存键/缓存头的设计。严格路由版本化 API 建议开启enableStrictParsing避免未匹配的 URL 被静默落到其他规则上。扩展阅读REST 路由详解、REST 快速上手 与本指南英文版 docs/guide/rest-versioning.md。小结Yii 2 的 REST API 版本控制策略清晰而务实主版本用模块与 URL 路径隔离v1、v2模块 yii\rest\UrlRule自动路由次版本用Accept头配合ContentNegotiator内容协商Response::acceptParams暴露版本参数。前者保证了不兼容变更的硬隔离后者保证了兼容增量的优雅演进两者结合既满足了第三方客户端对 BC 的诉求又为 API 的持续演进留足了空间。配置、目录结构与核心实现均可在 framework/rest/UrlRule.php、framework/filters/ContentNegotiator.php 与测试 tests/framework/filters/ContentNegotiatorTest.php 中继续深入研读。【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考