最简路径是执行 php artisan make:command BackupDatabaseCommand 后在 handle() 中调用 mysqldump,但必须校验权限、捕获错误、清理旧备份;需确保类继承正确、签名合法、命名空间匹配、依赖注入完整,并用 proc_open 安全执行、转义参数类型、限定存储路径、日志可追溯。

直接用 php artisan make:command BackupDatabaseCommand 生成骨架,再在 handle() 里调 shell_exec("mysqldump ...") 是最简路径;但不加权限校验、不处理错误码、不清理旧备份,上线后大概率出问题。
命令生成与注册必须走标准流程
别手写类文件丢进 app/Console/Commands/ 就以为完事。Laravel 9+ 虽支持自动发现,但前提是:类必须继承 Illuminate\Console\Command,$signature 属性非空,且命名空间与 PSR-4 路径严格匹配。跳过 make:command 生成器,容易漏掉构造函数调用 parent::__construct(),导致依赖注入失效。
- 执行
php artisan make:command BackupDatabaseCommand,生成文件在app/Console/Commands/BackupDatabaseCommand.php - 检查
protected $signature = 'backup:database {--compress} {--keep=7}';—— 选项名带冒号是错的,--compress合法,--backup:compress会报LogicException - 若改过目录结构或刚部署到新环境,跑一次
composer dump-autoload,否则自动发现可能失效 - 运行
php artisan list确认命令出现在backup:分组下;没出现就查use语句和extends是否写错
mysqldump 调用必须隔离敏感信息与错误路径
在 handle() 里硬拼 "mysqldump -u{$user} -p{$pass} ..." 是高危操作:密码会留在进程列表里,且 shell_exec() 失败时只返回 null,无法区分是权限拒绝、数据库宕机还是磁盘满。
- 改用
proc_open()捕获 stderr 输出,例如:$descriptors = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']]; - 从
config/database.php读取连接配置,别从.env直接取DB_PASSWORD——artisan在 console 环境下读取 env 是安全的,但明文拼接仍违反最小权限原则 - 导出路径强制限定在
storage_path('app/backups'),并提前用Filesystem确保目录存在且权限为755 - 执行前加
if (!extension_loaded('mysqli')) { $this->error('mysqldump 扩展未启用'); return 1; }
参数与选项处理稍有偏差就会静默失败
$this->option('compress') 返回 true 或 false,但 $this->option('keep') 返回字符串 "7",不是整数。如果直接传给 array_slice() 会触发类型警告,而 Artisan 默认不显示 PHP warning。
-
{--compress}是布尔开关,不接受等号赋值;--compress=1会被当成字符串"1",应写成{--compress=}才允许显式赋值 -
{--keep=7}的默认值7是字符串字面量,不能写成{--keep=Config::get('backup.keep_default')}——$signature是静态解析的,不支持运行时表达式 - 读取
--keep后必须手动转整型:$keep = (int) $this->option('keep');,否则intval("7\n")可能得0 - 可选参数如
{filename?}若未提供,$this->argument('filename')返回null,不是空字符串,判空要用is_null()或严格比较
备份生命周期管理不能靠人工清理
每天生成一个 SQL 文件却不删旧文件,三个月后 storage/app/backups/ 占满磁盘是常见事故。单纯用 Filesystem::files() 遍历删除,遇到几千个文件会超时,且没有原子性保障。
- 用
Storage::disk('local')->files('backups')获取文件列表,按修改时间排序,取倒序前$keep个之外的全部删除 - 删除前加
if (count($files) > $keep) { ... },避免每次执行都扫全量目录 - 压缩逻辑(如
gzip)不要塞进handle()主流程 —— 应另起独立命令或 job,防止单次执行耗时过长被系统 kill - 关键操作如
mysqldump成功后,立刻写一条日志到storage/logs/backup.log,包含时间戳、文件名、行数,方便巡检
真正难的不是调通 mysqldump,而是让命令在凌晨两点无人值守时,面对磁盘满、MySQL 拒绝连接、SELinux 限制、时区错位这些情况,依然能给出明确错误码并留下可追溯痕迹。签名写错一个字符、composer dump-autoload 忘了跑、umask 导致备份文件不可读——这些点单独看都小,合起来就是生产事故。


















