php artisan migrate:fresh 在生产环境会直接删除并重建数据库,不检查 app_env 实际值,也不执行 down() 方法,连框架自带表一并清除;必须通过入口层硬限制(如 appserviceprovider 中拦截 argv)或使用 junko5/laravel-safe-migration 强制确认来防护。

php artisan migrate:fresh 在生产环境会直接删库重建
这个命令不检查 APP_ENV 是否为 production,只依赖 .env 里 APP_ENV 的字符串值。一旦配置被误设为 local 或 testing,或者部署脚本硬编码了环境变量,migrate:fresh 就会在生产数据库上执行 DROP TABLE,且无回收站、无日志回溯。
它甚至不走任何迁移的 down() 方法,而是直接清空整个数据库(默认行为),连 personal_access_tokens 这类框架自带表也一并删除——而这类表往往由 vendor/ 下的迁移文件定义,你根本没在 database/migrations/ 里见过它。
- 不要指望 Laravel 自带防护:框架本身不拦截该命令
- 不要依赖 CI/CD 环境变量过滤:shell 脚本容易漏掉交互式终端或本地误触发
- 不要手动加 if (app()->environment('production')) { die(); }:Artisan 命令类被 autoload 加载时,环境可能尚未完全初始化,判断不可靠
用 junko5/laravel-safe-migration 拦截高危命令
这个包不是改命令逻辑,而是在命令真正执行前插入一个强制确认环节。它监听 migrate:rollback、migrate:reset、migrate:fresh 和 db:wipe 四类操作,一旦命中,立刻暂停,连接数据库查出当前所有表名和行数,生成类似这样的提示:
⚠️ 即将执行 migrate:fresh → 将删除 42 张表,共影响约 1,287,402 条记录 → 包括 users、orders、products 等核心表 → 你确定要继续吗? [y/N]
它不依赖环境变量判断,而是基于实时数据库状态做影响评估,哪怕你在 .env 里写了 APP_ENV=production,只要数据库里有真实数据,它就拦。
- 安装后无需修改任何迁移文件或命令调用方式
- 对 AI 编程助手(如 Cursor、Claude Code)特别有效:它们发出的命令会被原样捕获并阻断
- 确认环节是同步阻塞的,不会因异步钩子失效而跳过
schema dump 是提速手段,不是安全机制
php artisan schema:dump 生成的 schema.php 文件确实能让 migrate:fresh 从 8 分钟降到 3 秒,但它只是把“逐条跑迁移”换成“一次性执行原生 SQL”,**删库动作本身没变,危险等级也没降**。
如果你已经用了 schema dump,反而更容易忽略风险:速度快了,误操作成本变低,开发者更倾向于频繁执行 migrate:fresh,尤其在本地开发时习惯性 Ctrl+C/V 命令,忘了切换上下文。
- schema dump 不改变
migrate:fresh的语义:仍是 DROP + CREATE - 它无法防止你在 staging 环境误运行 —— staging 数据库通常有脱敏生产数据,删了照样业务中断
- dump 文件本身若被提交到 Git,还可能暴露表结构细节(如字段名、索引名)
真正可靠的防护必须在命令入口层做硬限制
靠文档、靠约定、靠 CI 检查都挡不住一次手抖。唯一能起作用的是让 migrate:fresh 在生产环境根本无法执行成功——不是弹窗确认,而是直接报错退出。
最简方案:在 app/Providers/AppServiceProvider.php 的 boot() 方法里加一段判断:
if (app()->environment('production') && in_array(
$_SERVER['argv'][1] ?? '',
['migrate:fresh', 'migrate:reset', 'migrate:rollback', 'db:wipe']
)) {
$this->alert('⛔ Refusing to run destructive command in production');
exit(1);
}
这段代码在 Artisan 启动早期就生效,比命令类实例化还早,且不依赖任何配置缓存或服务注册顺序。
- 注意:不能放在 register(),因为此时 $argv 还未完全可用
- $_SERVER['argv'] 比 $argv 全局变量更稳定,尤其在某些 SAPI(如 PHP-FPM)下
- exit(1) 比 throw Exception 更彻底,避免被 try/catch 吞掉
复杂点在于多环境部署时,有些 staging 环境其实也禁用 fresh,但环境变量写的是 staging;这种情况下,得额外维护一个禁止命令白名单数组,而不是只看 APP_ENV 字符串。











