返回博客

ThinkPHP .env Section 前缀陷阱:config() 静默返回默认值的真相

某天,某后台管理系统的内部 API 突然全量返回 403。日志显示认证失败,但运维反复确认:.env 里的密钥值和调用方配置一字不差。排查两小时,最后发现问题根本不在密钥值本身,而在 .env 文件里一个毫不起眼的 section 分组——密钥写在了错误的 [SECTION] 下方。

现象

  • config('app.internal_api_key') 返回的是 config/app.php 里写死的默认值 'your-secret-key-here',而非 .env 中配置的真实密钥
  • 直接调用 env('INTERNAL_API_KEY') 返回空字符串
  • 两端配置目视比对完全一致,认证仍然失败
  • 修改 .env 之后不重启也不报任何错误,只是静默地继续用默认值

根因

ThinkPHP 解析 .env 时,每个 key 都会被自动冠以它所在 [SECTION] 头的名称作为前缀。来看一个典型的 .env 文件片段:

[APP]
SECRET_KEY = abc123
 
[LANG]
default_lang = zh-cn
 
INTERNAL_API_KEY = my-secret   # ← 这行物理上跟在 [LANG] 后面!

ThinkPHP 解析后实际存储的 key 映射关系是:

| .env 写法 | 实际存储 key | |---|---| | [APP] 下的 SECRET_KEY | APP.SECRET_KEY | | [LANG] 下的 default_lang | LANG.DEFAULT_LANG | | [LANG] 之后的 INTERNAL_API_KEY | LANG.INTERNAL_API_KEY |

问题就在第三行:INTERNAL_API_KEY 虽然从视觉上看像是"游离"在 section 之外的顶级 key,但 ThinkPHP 会把它归属到最后一个出现的 [SECTION] 名下——这里是 LANG

于是 config/app.php 里的这行代码:

'internal_api_key' => env('INTERNAL_API_KEY', 'default'),

查找的是无前缀的 INTERNAL_API_KEY,实际根本不存在,所以永远走默认值分支,而且不抛异常、不打警告、不报错

坑:.env 的 key 归属于最后出现的 [SECTION],而非文件顶层

.env 文件里没有显式 section 声明的行,会被 ThinkPHP 归入最近一个 [SECTION] 名下。在 [LANG] 后随手加一行 API_KEY = xxx,实际注册的是 LANG.API_KEY,不是 API_KEYenv('API_KEY') 永远查不到,只会拿到默认值,且全程无任何报错。

正解

方法 A(推荐):把 key 移到正确的 section 下,config 用完整前缀读取

# .env
[APP]
SECRET_KEY = abc123
INTERNAL_API_KEY = my-secret   # ← 移到 [APP] 下
 
[LANG]
default_lang = zh-cn
// config/app.php
'internal_api_key' => env('APP.INTERNAL_API_KEY', 'default'),

方法 B:不动 .env,config 里补全实际前缀

// config/app.php
'internal_api_key' => env('LANG.INTERNAL_API_KEY', 'default'),

适合 .env 不方便改(如多环境共享)的情况,但语义上很奇怪,不推荐。

验证方式

用命令行快速探测实际存储的 key:

php -r "
require 'vendor/autoload.php';
\$app = new \think\App();
\$app->initialize();
// 先用带前缀的方式确认值是否存在
echo env('APP.INTERNAL_API_KEY');
// 再确认 config 层能否正确读到
echo config('app.internal_api_key');
"
正解:env() 调用时必须带 SECTION 前缀

ThinkPHP 的 env() 函数读 key 时,必须写 env('SECTION.KEY_NAME'),其中 SECTION 对应 .env 里的 [SECTION] 头(自动大写处理)。要么在 .env 里把 key 放到正确的 section,要么在 config/app.php 中用完整的带前缀写法。

附:think-multi-app 劫持单应用路由

同一次排查过程中还遇到了另一个静默炸弹:安装了 topthink/think-multi-app 包但项目是单应用结构,导致所有路由报"控制器不存在",route/app.php 中定义的路由完全不生效。

只需在 config/app.php 显式关掉自动多应用检测即可:

'auto_multi_app' => false,

think-multi-app 只要安装进 vendor,默认就会启用自动多应用模式,不管项目结构是否真的多应用。

一句话外卖

ThinkPHP .env 里的每一行都要问自己"我在哪个 [SECTION] 下?"——不在任何 section 下不代表是顶级 key,而是归属于上一个 section,env() 必须带完整前缀才能查到。