核心问题是CLI与Web端PHP版本一致性及模块路由不被误识别为路径别名;需确保phpEnv切换后重启Web服务,并将模块ID设为apiV1/apiV2而非v1/v2,同时显式声明路由前缀并关闭enableStrictParsing。

phpEnv 下 Yii2 多版本共存,核心问题不是“能不能”,而是「CLI 和 Web 两端 PHP 版本是否真正一致」+「模块路由是否被误识别为路径别名」。只要这两点踩准,v1/v2 就能稳定并行。
phpEnv 中确认 CLI 和 Web 使用的是同一 PHP 版本
phpEnv 是多版本切换工具,但默认只改 PATH,不自动同步 Apache/Nginx 的 PHP SAPI 模块。常见现象是:php -v 显示 8.1,而 phpinfo() 显示 7.4 —— 这会导致 Yii2 启动时部分扩展(如 mbstring、json)加载失败,或 composer 安装的包因 PHP 版本约束被跳过。
- 执行
which php和php -i | grep "Loaded Configuration File",确认 CLI 加载的是你期望的php.ini - 在 Web 环境下访问
web/index.php,输出phpinfo(),搜索Configuration File (php.ini) Path和PHP Version - 若不一致:phpEnv 切换后,必须手动重启 Apache/Nginx(比如
sudo apachectl restart),否则 Web 仍用旧模块 - 特别注意 vendor 包依赖:运行
composer show | grep -E "(php|symfony|guzzle)",检查关键包(如symfony/polyfill-php81)是否真被加载;有些 polyfill 包会在 PHP 版本不匹配时静默失效
Yii2 模块化多版本路由配置(避免 v1 被当模块 ID 解析)
在 phpEnv + Yii2 高级模板中,直接把 v1 当作 module ID 是高危操作。框架会优先尝试加载 modules/v1/Module.php,而不是走你写的 'v1/<controller>' => 'v1/<controller>'</controller></controller> 规则 —— 结果就是 404 或控制器错乱。
- module ID 改用语义化名称,例如
'apiV1' => ['class' => 'api\modules\v1\Module'],而非'v1' - urlManager.rules 中显式声明前缀路由:
'v1/<controller:>/<id:>' => 'api-v1/<controller>/view'</controller></id:></controller:>,同时设'enableStrictParsing' => false - 每个模块的
Module.php中必须显式定义controllerNamespace,例如public $controllerNamespace = 'api\modules\v1\controllers';,不能依赖默认值 - 别在
common/config/bootstrap.php里用Yii::setAlias('@v1', ...),容易和实际模块路径冲突;别名应按功能命名,如@apiV1Assets
BaseRestController 中 session 和 authenticator 的开关逻辑
v1 和 v2 接口可能共用一套 User 组件,但 REST 场景下 session 启动失败会导致整个请求中断(尤其 CLI 执行 yii queue/run 时),而关掉 user 组件又会让 $user->loginByAccessToken() 报错。
立即学习“PHP免费学习笔记(深入)”;
- 在模块基类(如
api\modules\v1\controllers\BaseController)中重写init(): - 调用
Yii::$app->user->enableSession = false;关 session,但保留Yii::$app->user实例 - 在
behaviors()中禁用'authenticator',但不要 unset'user'行为 —— 否则Yii::$app->user->identity会为空 - 若某接口确实无需鉴权,继承
yii\rest\Controller,而不是在 behaviors 里临时关 authenticator;混用会导致行为链执行顺序异常 -
$request->get('token')返回null,不是false或空字符串,判空必须用!== null,否则0、''等合法值会被误拒
共用模型时如何避免 v2 修改破坏 v1 兼容性
直接让 v1 和 v2 controller 调用同一个 UserModel 是最省事的做法,但也是最危险的——字段加个 NOT NULL,v1 的 POST 就 500。
- 定义契约接口,例如
interface ApiUserContract { public function getDisplayName(): string; } - v1 和 v2 各自实现该接口:
class V1User implements ApiUserContract、class V2User implements ApiUserContract - 控制器里只依赖接口,不 new 具体类:
public function actionView($id) { $user = $this->userFactory->make($id, 'v1'); return $user->getDisplayName(); } - 数据库迁移必须按版本拆分:v1 的 migration 放
migrations/v1/,v2 的放migrations/v2/,执行时指定yii migrate --migrationPath=@api/modules/v2/migrations
真正难的不是配路由或切模块,而是当 v2 新增一个字段校验规则时,如何确保它绝不会漏进 v1 的请求生命周期里 —— 所有共享层(模型、行为、过滤器)都得有明确的版本感知能力,而不是靠注释或文档约定。



















