最稳妥的做法是在基控制器 init() 中通过正则解析 Yii::$app->requestedRoute 提取路径前缀版本号(如 v1/v2),存入 $this->version 并设默认值,避免依赖模块ID、Header 或手动解析 URL。

Yii 框架本身不自动从 URL、Header 或请求体中提取并设置 $version 变量——你得自己决定在哪一层识别版本号,并把它传给后续逻辑。最稳妥、最常用的做法是:在路由解析后、控制器执行前,通过路径前缀(如 /v1/user)识别版本,并存入 Yii::$app->params['apiVersion'] 或注入到控制器属性中。
怎么从 URL 路径里提取 v1/v2 版本号
别依赖 $_GET 或手动 explode($_SERVER['REQUEST_URI'], '/')——这容易漏掉重写规则、带查询参数的场景,且破坏 Yii 的请求生命周期。正确方式是在 UrlRule 或 UrlManager 的 on afterParse 事件里捕获,或更轻量地:在基控制器的 init() 中解析 Yii::$app->requestedRoute。
-
Yii::$app->requestedRoute是框架解析后的路由(如v1/user/index),比原始 URL 更可靠 - 用
preg_match('/^v(\d+)\/(.+)$/', Yii::$app->requestedRoute, $matches)提取版本数字和剩余路由段 - 匹配失败时应设默认版本(如
v1)或直接 throw new BadRequestHttpException() - 避免在每个控制器里重复写正则——抽成一个
BaseApiController::detectApiVersion()方法
为什么不能靠模块 ID(如 v1)来判断版本
模块 ID 是配置项,不是运行时上下文。当你把模块 ID 设为 v1,又在路由里配了 'v1/<controller>' => 'v1/<controller>'</controller></controller>,框架会优先尝试加载 v1 模块,但一旦你加了另一个同名模块或路由冲突,Yii::$app->getModule('v1') 可能返回 null 或错模块——版本识别就断了。
- 模块 ID 应该语义清晰,比如
apiV1、apiV2,和路由前缀v1/解耦 -
Yii::$app->getModule('v1')在非模块请求(如控制台命令)中根本不可用 - 模块可能未加载(lazy load)、被禁用,或在某些中间件中尚未初始化
- 真正需要版本号的地方(如模型字段兼容性判断、日志打标)必须基于已确定的运行时值,而不是配置名
如何让控制器和模型感知当前 API 版本
不要在每个 action 里重复解析,也不要靠全局常量(改起来反人类)。推荐两种实际可用的方式:
- 在基控制器
init()中调用$this->version = $this->detectApiVersion(),然后所有子控制器都能用$this->version - 在服务层(如
UserQueryService)构造时注入版本号:new UserQueryService($this->version),避免模型里硬编码v1/v2分支 - 共用模型若需差异化行为(如 v1 返回
created_at字符串,v2 返回时间戳),用接口契约 + 工厂模式:ApiUserResponseFactory::make($this->version)->format($user) - 禁止在
rules()或fields()方法里写if ($this->version === 'v2') {...}——这会让验证逻辑和序列化逻辑混杂,难测试也难复用
常见错误:用 Accept Header 或自定义 Header 做版本识别
HTTP Accept: application/vnd.myapp.v2+json 看起来很 RESTful,但在 Yii 实际项目中极易失控:
- 前端发错 header(大小写、空格、拼写)导致静默 fallback 到 v1,问题难复现 Yii 的
- 调试时 curl 不带 header 就进错版本,Postman 配置遗漏也一样
- CDN、网关、反向代理可能 strip 或改写 Accept,导致线上环境行为不一致
UrlManager 和 ContentNegotiator 默认不按 Accept 解析版本,得自己写 beforeAction 拦截并覆盖 Yii::$app->requestedRoute,极易和缓存、CORS、认证中间件打架
路径前缀虽“不那么 RESTful”,但稳定、可读、可缓存、可监控——95% 的 Yii 生产项目都选它。真要支持多方式,也得把路径前缀作为 fallback 主干,Header 仅作可选补充。


















