先给结论:如果你在 ThinkPHP 8.0 的 CLI 模式下(例如运行 php think migrate)发现 .env 文件完全无法生效,请不要急于质疑自己的配置写法——绝大多数情况下,文件压根没有被框架加载。系统不会抛出任何错误,只会静默忽略你的修改。即使你已经将 APP_DEBUG 改为 true,调用 Env::get('APP_DEBUG') 返回的结果依然是 null。这个常见问题在社区中被反复提及,根源其实非常清晰。

为什么 .env 在 CLI 下完全不生效?
ThinkPHP 8.0 默认仅在其 Web 入口文件(public/index.php)中加载 .env,CLI 模式并不会走这一加载流程。这不是疏忽,而是框架设计上认为 CLI 环境下的环境变量应当由操作系统或容器直接注入。
- 解决方案:在项目根目录下的
think文件末尾手动添加加载逻辑:if (PHP_SAPI === 'cli') { DotenvDotenv::createImmutable(__DIR__)->safeLoad(); }。 - 关键点:
__DIR__必须指向项目根目录(即包含app/、config/、composer.json的目录),而不是public/或vendor/。 - 定时任务(如 cron)也容易踩坑:必须采用
cd /path/to/project && php think xxx这种写法,不要直接用绝对路径调用public/index.php。 - 一个容易被忽略的陷阱:编辑器保存
.env文件时请务必选择“UTF-8 无 BOM”格式。Windows 记事本默认带 BOM,会导致safeLoad()静默失败——你修改了半天,它却视而不见。
Env::get('DB_HOST') 返回 null,但 getenv('DB_HOST') 有值
这并非环境变量未设置,而是 ThinkPHP 的白名单机制在起作用。自 5.1 版本开始,Env::get() 默认只允许访问 APP_ENV、APP_DEBUG 等几个预定义键名,你自己定义的 DB_HOST、REDIS_URL 并不在列表内,因此直接返回 null。
- 有两个解决办法:一是在
thinkinitializerEnv初始化之前调用thinkEnv::setWhitelist(['DB_HOST', 'REDIS_URL']);二是改用原生getenv('DB_HOST')(注意php.ini中variables_order必须包含E)。 - 更推荐的做法:在配置文件中统一使用
env('DB_HOST', '127.0.0.1')辅助函数。该env()函数内部会绕过白名单,直接查询$_ENV和getenv()的值,使用起来更加省心。 - 注意不要在
bootstrap.php或config/目录下的文件中提前调用Env::get()——此时.env文件尚未加载,必然返回null。
多环境配置为何不按 APP_ENV=testing 切换?
ThinkPHP 的“多环境”机制并非通过更换 .env 文件名(例如 .env.testing)来实现,而是依据 APP_ENV 的值去合并 config/testing/ 目录下同名配置文件中的设置。如果 config/testing/app.php 不存在,框架会当作无事发生,既不报错,也不回退。
APP_ENV必须通过系统环境变量进行设置(例如export APP_ENV=testing或 Docker 中使用-e APP_ENV=testing),用define('APP_ENV', 'testing')无效,该值会被框架覆盖。config/{APP_ENV}/目录下至少需要存在一个与基础配置同名的文件(比如app.php),即便其内容只含空数组[],否则该环境配置完全不会参与合并。.env和config/{APP_ENV}/之间是并存关系:前者提供原始字符串值,后者决定哪些配置项被覆盖;两者互不排斥,也不替代对方。- 修改
.env或新增环境配置后,必须清空runtime/cache/目录,否则旧缓存会掩盖你所作出的变更。
最常被忽视的一点:在 Swoole、RoadRunner 这类常驻进程中,.env 仅在启动时读取一次。修改文件后如果不重启服务,修改就等于无效。
