命令行工具
DuxLite 提供了强大的命令行工具系统,基于 Symfony Console 组件,支持自定义命令开发、自动发现和注册机制。通过命令行工具,您可以执行数据迁移、定时任务、维护脚本等各种操作。
设计理念
DuxLite 命令行系统采用简单、高效、可扩展的命令行工具设计:
- 自动发现:通过注解自动发现和注册命令
- 标准化接口:基于 Symfony Console 的标准化命令接口
- 依赖注入:命令类支持依赖注入和服务容器
- 灵活配置:支持丰富的参数选项和交互式输入
- 错误处理:完整的异常处理和错误报告机制
核心组件
- Command 类:命令系统核心类,提供注册和发现功能
- Console 应用:基于 Symfony Console 的命令行应用程序
- Command 注解:用于标记自定义命令类的注解
- 属性扫描:自动扫描带注解的命令类
命令注册和发现
自动命令发现
DuxLite 使用注解来自动发现和注册命令:
php
use Core\Command\Command;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[Command]
class ExampleCommand extends BaseCommand
{
protected static $defaultName = 'app:example';
protected static $defaultDescription = '示例命令';
protected function configure(): void
{
$this
->setName('app:example')
->setDescription('这是一个示例命令')
->setHelp('此命令演示如何创建自定义命令');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Hello from example command!');
return Command::SUCCESS;
}
}
手动命令注册
1. 在 App.php 应用入口的 init 方法中注册
php
// 在应用模块的 init() 方法中注册命令
use Core\Command\Command;
class App extends AppExtend
{
public function init(Bootstrap $bootstrap): void
{
// 在 init 方法中注册自定义命令
// 注意:这里是通过回调机制实现的
$commands = [
\App\Commands\DatabaseMigrateCommand::class,
\App\Commands\CacheClearCommand::class,
\App\Commands\QueueProcessCommand::class,
];
// 将命令添加到全局配置中
// 具体实现需要根据框架的回调机制
}
}
2. 通过 command.toml 文件配置
toml
# config/command.toml
registers = [
"App\\Commands\\DatabaseMigrateCommand",
"App\\Commands\\CacheClearCommand",
"App\\Commands\\QueueProcessCommand",
"App\\Commands\\CustomCommand"
]
3. 框架加载流程
php
// 框架在 Bootstrap::loadCommand() 中自动加载
public function loadCommand(): void
{
// 1. 从配置文件读取命令
$commands = App::config("command")->get("registers", []);
// 2. 添加框架内置命令
$commands[] = BackupCommand::class;
$commands[] = RestoreCommand::class;
$commands[] = PermissionCommand::class;
// ...
// 3. 添加注解发现的命令
$commands = [...$commands, ...Command::registerAttribute()];
// 4. 初始化命令行应用
$this->command = Command::init($commands);
}
基础命令开发
简单命令示例
php
use Core\Command\Command;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[Command]
class GreetCommand extends BaseCommand
{
protected static $defaultName = 'app:greet';
protected function configure(): void
{
$this
->setName('app:greet')
->setDescription('向用户打招呼')
->setHelp('此命令可以向指定用户打招呼')
// 必需参数
->addArgument(
'name',
InputArgument::REQUIRED,
'用户名称'
)
// 可选参数
->addArgument(
'lastName',
InputArgument::OPTIONAL,
'用户姓氏'
)
// 选项
->addOption(
'uppercase',
'u',
InputOption::VALUE_NONE,
'输出大写字母'
)
->addOption(
'format',
'f',
InputOption::VALUE_REQUIRED,
'输出格式',
'default'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// 获取参数
$name = $input->getArgument('name');
$lastName = $input->getArgument('lastName');
// 获取选项
$uppercase = $input->getOption('uppercase');
$format = $input->getOption('format');
// 构建问候语
$fullName = $lastName ? "$name $lastName" : $name;
$greeting = match($format) {
'formal' => "尊敬的 $fullName 先生/女士,您好!",
'casual' => "嗨,$fullName!",
default => "您好,$fullName!"
};
// 处理大写选项
if ($uppercase) {
$greeting = strtoupper($greeting);
}
// 输出结果
$output->writeln($greeting);
return Command::SUCCESS;
}
}
交互式命令
php
use Core\Command\Command;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
#[Command]
class InteractiveCommand extends BaseCommand
{
protected static $defaultName = 'app:interactive';
protected function configure(): void
{
$this
->setName('app:interactive')
->setDescription('交互式示例命令')
->setHelp('演示各种交互式输入方式');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
// 文本输入
$nameQuestion = new Question('请输入您的姓名: ');
$nameQuestion->setValidator(function ($answer) {
if (trim($answer) == '') {
throw new \RuntimeException('姓名不能为空');
}
return $answer;
});
$name = $helper->ask($input, $output, $nameQuestion);
// 密码输入(隐藏输入)
$passwordQuestion = new Question('请输入密码: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$password = $helper->ask($input, $output, $passwordQuestion);
// 选择题
$typeQuestion = new ChoiceQuestion(
'请选择账户类型:',
['admin' => '管理员', 'user' => '普通用户', 'guest' => '访客'],
'user'
);
$typeQuestion->setErrorMessage('选择 %s 无效');
$userType = $helper->ask($input, $output, $typeQuestion);
// 确认问题
$confirmQuestion = new ConfirmationQuestion(
"确认创建用户 $name (类型: $userType)? [y/N] ",
false
);
$confirmed = $helper->ask($input, $output, $confirmQuestion);
if ($confirmed) {
$output->writeln("✅ 用户 $name 创建成功!");
$output->writeln("类型: $userType");
return Command::SUCCESS;
} else {
$output->writeln("❌ 操作已取消");
return Command::FAILURE;
}
}
}
实用命令示例
数据库迁移命令
php
use Core\Command\Command;
use Core\App;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
#[Command]
class DatabaseMigrateCommand extends BaseCommand
{
protected static $defaultName = 'db:migrate';
protected function configure(): void
{
$this
->setName('db:migrate')
->setDescription('运行数据库迁移')
->addOption(
'fresh',
null,
InputOption::VALUE_NONE,
'删除所有表并重新迁移'
)
->addOption(
'seed',
null,
InputOption::VALUE_NONE,
'迁移后运行数据填充'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('🚀 开始数据库迁移...');
try {
// 检查数据库连接
App::db()->connection()->getPdo();
$output->writeln('✅ 数据库连接成功');
$fresh = $input->getOption('fresh');
$seed = $input->getOption('seed');
if ($fresh) {
$output->writeln('🔄 删除所有表...');
$this->dropAllTables();
$output->writeln('✅ 所有表已删除');
}
// 创建迁移表
$this->createMigrationTable();
// 运行迁移
$migrations = $this->getMigrations();
$executed = $this->getExecutedMigrations();
foreach ($migrations as $migration) {
if (!in_array($migration, $executed)) {
$output->writeln("📦 运行迁移: $migration");
$this->runMigration($migration);
$this->recordMigration($migration);
$output->writeln("✅ 迁移完成: $migration");
}
}
if ($seed) {
$output->writeln('🌱 开始数据填充...');
$this->runSeeders();
$output->writeln('✅ 数据填充完成');
}
$output->writeln('🎉 数据库迁移成功完成!');
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("❌ 迁移失败: " . $e->getMessage());
return Command::FAILURE;
}
}
private function createMigrationTable(): void
{
if (!Schema::hasTable('migrations')) {
Schema::create('migrations', function (Blueprint $table) {
$table->id();
$table->string('migration');
$table->integer('batch');
$table->timestamp('created_at')->useCurrent();
});
}
}
private function getMigrations(): array
{
$migrationPath = base_path('database/migrations');
if (!is_dir($migrationPath)) {
return [];
}
$files = scandir($migrationPath);
$migrations = [];
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$migrations[] = pathinfo($file, PATHINFO_FILENAME);
}
}
sort($migrations);
return $migrations;
}
private function getExecutedMigrations(): array
{
try {
return App::db()->table('migrations')
->pluck('migration')
->toArray();
} catch (\Exception $e) {
return [];
}
}
private function runMigration(string $migration): void
{
$migrationFile = base_path("database/migrations/{$migration}.php");
if (file_exists($migrationFile)) {
require_once $migrationFile;
// 假设迁移文件包含 up() 函数
if (function_exists('up')) {
up();
}
}
}
private function recordMigration(string $migration): void
{
$batch = App::db()->table('migrations')->max('batch') + 1;
App::db()->table('migrations')->insert([
'migration' => $migration,
'batch' => $batch,
'created_at' => date('Y-m-d H:i:s')
]);
}
private function dropAllTables(): void
{
$tables = App::db()->select('SHOW TABLES');
foreach ($tables as $table) {
$tableName = array_values((array)$table)[0];
Schema::dropIfExists($tableName);
}
}
private function runSeeders(): void
{
$seederPath = base_path('database/seeders');
if (!is_dir($seederPath)) {
return;
}
$files = scandir($seederPath);
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$seederFile = "$seederPath/$file";
require_once $seederFile;
}
}
}
}
缓存管理命令
php
use Core\Command\Command;
use Core\App;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[Command]
class CacheCommand extends BaseCommand
{
protected static $defaultName = 'cache:clear';
protected function configure(): void
{
$this
->setName('cache:clear')
->setDescription('清理缓存')
->addArgument(
'type',
InputArgument::OPTIONAL,
'缓存类型 (all, app, views, config)',
'all'
)
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'强制清理,不提示确认'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$type = $input->getArgument('type');
$force = $input->getOption('force');
if (!$force) {
$helper = $this->getHelper('question');
$question = new \Symfony\Component\Console\Question\ConfirmationQuestion(
"确认清理 {$type} 缓存? [y/N] ",
false
);
if (!$helper->ask($input, $output, $question)) {
$output->writeln('❌ 操作已取消');
return Command::SUCCESS;
}
}
$output->writeln("🧹 开始清理 {$type} 缓存...");
try {
switch ($type) {
case 'app':
$this->clearAppCache($output);
break;
case 'views':
$this->clearViewCache($output);
break;
case 'config':
$this->clearConfigCache($output);
break;
case 'all':
default:
$this->clearAppCache($output);
$this->clearViewCache($output);
$this->clearConfigCache($output);
break;
}
$output->writeln('✅ 缓存清理完成!');
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("❌ 缓存清理失败: " . $e->getMessage());
return Command::FAILURE;
}
}
private function clearAppCache(OutputInterface $output): void
{
$cache = App::cache();
$cache->clear();
$output->writeln('📦 应用缓存已清理');
}
private function clearViewCache(OutputInterface $output): void
{
$viewCachePath = data_path('cache/views');
if (is_dir($viewCachePath)) {
$this->deleteDirectory($viewCachePath);
mkdir($viewCachePath, 0755, true);
}
$output->writeln('🎨 视图缓存已清理');
}
private function clearConfigCache(OutputInterface $output): void
{
$configCachePath = data_path('cache/config');
if (is_dir($configCachePath)) {
$this->deleteDirectory($configCachePath);
mkdir($configCachePath, 0755, true);
}
$output->writeln('⚙️ 配置缓存已清理');
}
private function deleteDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = "$dir/$file";
if (is_dir($path)) {
$this->deleteDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}
队列处理命令
php
use Core\Command\Command;
use Core\App;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
#[Command]
class QueueWorkerCommand extends BaseCommand
{
protected static $defaultName = 'queue:work';
private bool $shouldStop = false;
protected function configure(): void
{
$this
->setName('queue:work')
->setDescription('处理队列任务')
->addOption(
'queue',
'q',
InputOption::VALUE_REQUIRED,
'处理的队列名称',
'default'
)
->addOption(
'delay',
'd',
InputOption::VALUE_REQUIRED,
'失败任务重试延迟(秒)',
'3'
)
->addOption(
'memory',
'm',
InputOption::VALUE_REQUIRED,
'内存限制(MB)',
'128'
)
->addOption(
'timeout',
't',
InputOption::VALUE_REQUIRED,
'任务超时时间(秒)',
'60'
)
->addOption(
'tries',
'r',
InputOption::VALUE_REQUIRED,
'最大重试次数',
'3'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$queueName = $input->getOption('queue');
$delay = (int) $input->getOption('delay');
$memoryLimit = (int) $input->getOption('memory');
$timeout = (int) $input->getOption('timeout');
$maxTries = (int) $input->getOption('tries');
$output->writeln("🚀 队列工作进程启动");
$output->writeln("📋 队列: {$queueName}");
$output->writeln("⏱️ 超时: {$timeout}s");
$output->writeln("💾 内存限制: {$memoryLimit}MB");
$output->writeln("🔄 最大重试: {$maxTries}次");
// 注册信号处理器
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
pcntl_signal(SIGINT, [$this, 'handleSignal']);
}
$startTime = time();
$processedJobs = 0;
while (!$this->shouldStop) {
// 检查内存使用
if ($this->exceedsMemoryLimit($memoryLimit)) {
$output->writeln("⚠️ 内存使用超限,停止进程");
break;
}
// 处理信号
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
try {
// 获取队列任务(这里简化处理)
$job = $this->getNextJob($queueName);
if ($job) {
$output->writeln("📝 处理任务: {$job['id']}");
$jobStartTime = microtime(true);
$success = $this->processJob($job, $timeout);
$executionTime = round((microtime(true) - $jobStartTime) * 1000, 2);
if ($success) {
$this->markJobCompleted($job);
$processedJobs++;
$output->writeln("✅ 任务完成: {$job['id']} ({$executionTime}ms)");
} else {
$this->handleFailedJob($job, $maxTries, $delay);
$output->writeln("❌ 任务失败: {$job['id']}");
}
} else {
// 没有任务,休眠1秒
sleep(1);
}
} catch (\Exception $e) {
$output->writeln("💥 处理异常: " . $e->getMessage());
sleep($delay);
}
}
$runtime = time() - $startTime;
$output->writeln("🛑 队列工作进程停止");
$output->writeln("📊 统计: 运行时间 {$runtime}s,处理任务 {$processedJobs} 个");
return Command::SUCCESS;
}
public function handleSignal(int $signal): void
{
$this->shouldStop = true;
}
private function exceedsMemoryLimit(int $limitMB): bool
{
$memoryUsage = memory_get_usage(true) / 1024 / 1024;
return $memoryUsage > $limitMB;
}
private function getNextJob(string $queueName): ?array
{
// 简化的队列任务获取逻辑
try {
$job = App::db()->table('jobs')
->where('queue', $queueName)
->where('reserved_at', null)
->where('available_at', '<=', time())
->orderBy('id')
->first();
if ($job) {
// 标记任务为已保留
App::db()->table('jobs')
->where('id', $job->id)
->update(['reserved_at' => time()]);
return (array) $job;
}
} catch (\Exception $e) {
return null;
}
return null;
}
private function processJob(array $job, int $timeout): bool
{
try {
// 设置超时
set_time_limit($timeout);
// 反序列化并执行任务
$payload = json_decode($job['payload'], true);
$jobClass = $payload['job'] ?? null;
if ($jobClass && class_exists($jobClass)) {
$jobInstance = new $jobClass();
if (method_exists($jobInstance, 'handle')) {
$jobInstance->handle();
return true;
}
}
return false;
} catch (\Exception $e) {
error_log("Job execution failed: " . $e->getMessage());
return false;
}
}
private function markJobCompleted(array $job): void
{
App::db()->table('jobs')->where('id', $job['id'])->delete();
}
private function handleFailedJob(array $job, int $maxTries, int $delay): void
{
$attempts = ($job['attempts'] ?? 0) + 1;
if ($attempts >= $maxTries) {
// 移到失败队列
App::db()->table('failed_jobs')->insert([
'uuid' => $job['uuid'] ?? uniqid(),
'connection' => 'database',
'queue' => $job['queue'],
'payload' => $job['payload'],
'exception' => 'Max attempts exceeded',
'failed_at' => date('Y-m-d H:i:s')
]);
App::db()->table('jobs')->where('id', $job['id'])->delete();
} else {
// 增加重试次数,设置延迟
App::db()->table('jobs')
->where('id', $job['id'])
->update([
'attempts' => $attempts,
'reserved_at' => null,
'available_at' => time() + $delay
]);
}
}
}
定时任务命令
任务调度命令
php
use Core\Command\Command;
use Core\App;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
#[Command]
class ScheduleCommand extends BaseCommand
{
protected static $defaultName = 'schedule:run';
protected function configure(): void
{
$this
->setName('schedule:run')
->setDescription('运行定时任务')
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'强制运行所有任务'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$force = $input->getOption('force');
$output->writeln('⏰ 开始检查定时任务...');
try {
$tasks = $this->getScheduledTasks();
$executedCount = 0;
foreach ($tasks as $task) {
if ($force || $this->shouldRunTask($task)) {
$output->writeln("🚀 执行任务: {$task['name']}");
$startTime = microtime(true);
$success = $this->executeTask($task);
$duration = round((microtime(true) - $startTime) * 1000, 2);
if ($success) {
$output->writeln("✅ 任务完成: {$task['name']} ({$duration}ms)");
$this->recordTaskExecution($task, true, $duration);
} else {
$output->writeln("❌ 任务失败: {$task['name']}");
$this->recordTaskExecution($task, false, $duration);
}
$executedCount++;
}
}
if ($executedCount === 0) {
$output->writeln('💤 没有需要执行的任务');
} else {
$output->writeln("🎉 完成 {$executedCount} 个定时任务");
}
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("❌ 定时任务执行失败: " . $e->getMessage());
return Command::FAILURE;
}
}
private function getScheduledTasks(): array
{
return [
[
'name' => 'cache_cleanup',
'command' => 'cache:clear',
'schedule' => '0 2 * * *', // 每天凌晨2点
'description' => '清理过期缓存'
],
[
'name' => 'log_rotation',
'command' => 'log:rotate',
'schedule' => '0 0 * * 0', // 每周日凌晨
'description' => '日志轮转'
],
[
'name' => 'backup_database',
'command' => 'db:backup',
'schedule' => '0 3 * * *', // 每天凌晨3点
'description' => '数据库备份'
]
];
}
private function shouldRunTask(array $task): bool
{
// 检查上次执行时间
$lastRun = $this->getLastTaskRun($task['name']);
$schedule = $task['schedule'];
// 简化的cron表达式解析
return $this->cronMatches($schedule, $lastRun);
}
private function executeTask(array $task): bool
{
try {
// 执行命令
$command = $task['command'];
$application = Command::init([]);
// 这里简化处理,实际应该调用相应的命令
switch ($command) {
case 'cache:clear':
App::cache()->clear();
break;
case 'log:rotate':
$this->rotateLogFiles();
break;
case 'db:backup':
$this->backupDatabase();
break;
default:
return false;
}
return true;
} catch (\Exception $e) {
error_log("Scheduled task failed: " . $e->getMessage());
return false;
}
}
private function recordTaskExecution(array $task, bool $success, float $duration): void
{
App::db()->table('task_logs')->insert([
'task_name' => $task['name'],
'command' => $task['command'],
'success' => $success,
'duration_ms' => $duration,
'executed_at' => date('Y-m-d H:i:s')
]);
}
private function getLastTaskRun(string $taskName): ?string
{
$lastRun = App::db()->table('task_logs')
->where('task_name', $taskName)
->where('success', true)
->orderBy('executed_at', 'desc')
->first();
return $lastRun ? $lastRun->executed_at : null;
}
private function cronMatches(string $schedule, ?string $lastRun): bool
{
// 简化的cron匹配逻辑
// 实际应该使用专门的cron表达式解析库
$now = time();
$lastRunTime = $lastRun ? strtotime($lastRun) : 0;
// 示例:每小时检查一次
return ($now - $lastRunTime) >= 3600;
}
private function rotateLogFiles(): void
{
$logPath = data_path('logs');
if (!is_dir($logPath)) {
return;
}
$files = glob("$logPath/*.log");
foreach ($files as $file) {
if (filesize($file) > 10 * 1024 * 1024) { // 10MB
$rotatedFile = $file . '.' . date('Y-m-d-H-i-s');
rename($file, $rotatedFile);
gzopen($rotatedFile . '.gz', 'w');
unlink($rotatedFile);
}
}
}
private function backupDatabase(): void
{
$config = App::config('database');
$database = $config['database'];
$username = $config['username'];
$password = $config['password'];
$host = $config['host'];
$backupFile = data_path("backups/database_" . date('Y-m-d_H-i-s') . '.sql');
$backupDir = dirname($backupFile);
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
}
$command = "mysqldump -h$host -u$username -p$password $database > $backupFile";
exec($command, $output_lines, $return_code);
if ($return_code !== 0) {
throw new \RuntimeException('数据库备份失败');
}
exec("gzip $backupFile");
$output->writeln('✅ 数据库备份完成');
}
}
应用发布命令
部署和维护命令
php
use Core\Command\Command;
use Core\App;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
#[Command]
class DeployCommand extends BaseCommand
{
protected static $defaultName = 'app:deploy';
protected function configure(): void
{
$this
->setName('app:deploy')
->setDescription('应用部署')
->addOption(
'env',
'e',
InputOption::VALUE_REQUIRED,
'部署环境',
'production'
)
->addOption(
'skip-backup',
null,
InputOption::VALUE_NONE,
'跳过备份'
)
->addOption(
'skip-migrate',
null,
InputOption::VALUE_NONE,
'跳过数据库迁移'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$env = $input->getOption('env');
$skipBackup = $input->getOption('skip-backup');
$skipMigrate = $input->getOption('skip-migrate');
$output->writeln("🚀 开始部署到 {$env} 环境...");
try {
// 1. 检查环境
$this->checkEnvironment($output, $env);
// 2. 备份数据库
if (!$skipBackup) {
$this->backupDatabase($output);
}
// 3. 清理缓存
$this->clearCache($output);
// 4. 数据库迁移
if (!$skipMigrate) {
$this->runMigrations($output);
}
// 5. 优化应用
$this->optimizeApplication($output);
// 6. 重启服务
$this->restartServices($output, $env);
$output->writeln('🎉 部署完成!');
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("❌ 部署失败: " . $e->getMessage());
return Command::FAILURE;
}
}
private function checkEnvironment(OutputInterface $output, string $env): void
{
$output->writeln('🔍 检查运行环境...');
// 检查PHP版本
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
throw new \RuntimeException('PHP版本需要8.1或更高');
}
// 检查必需扩展
$requiredExtensions = ['pdo', 'mbstring', 'openssl', 'curl'];
foreach ($requiredExtensions as $ext) {
if (!extension_loaded($ext)) {
throw new \RuntimeException("缺少必需的PHP扩展: {$ext}");
}
}
// 检查目录权限
$writableDirs = [data_path(), data_path('cache'), data_path('logs')];
foreach ($writableDirs as $dir) {
if (!is_writable($dir)) {
throw new \RuntimeException("目录不可写: {$dir}");
}
}
$output->writeln('✅ 环境检查通过');
}
private function backupDatabase(OutputInterface $output): void
{
$output->writeln('💾 备份数据库...');
$config = App::config('database');
$backupFile = data_path('backups/pre-deploy-' . date('Y-m-d-H-i-s') . '.sql');
$backupDir = dirname($backupFile);
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
}
$command = sprintf(
'mysqldump -h%s -u%s -p%s %s > %s',
$config['host'],
$config['username'],
$config['password'],
$config['database'],
$backupFile
);
exec($command, $output_lines, $return_code);
if ($return_code !== 0) {
throw new \RuntimeException('数据库备份失败');
}
exec("gzip {$backupFile}");
$output->writeln('✅ 数据库备份完成');
}
private function clearCache(OutputInterface $output): void
{
$output->writeln('🧹 清理缓存...');
App::cache()->clear();
$cacheDirs = [
data_path('cache/views'),
data_path('cache/config'),
data_path('cache/routes')
];
foreach ($cacheDirs as $dir) {
if (is_dir($dir)) {
$this->deleteDirectory($dir);
mkdir($dir, 0755, true);
}
}
$output->writeln('✅ 缓存清理完成');
}
private function runMigrations(OutputInterface $output): void
{
$output->writeln('📦 运行数据库迁移...');
// 这里应该调用迁移命令
// 简化处理
$migrations = $this->getPendingMigrations();
foreach ($migrations as $migration) {
$output->writeln(" 📄 {$migration}");
$this->runMigration($migration);
}
$output->writeln('✅ 数据库迁移完成');
}
private function optimizeApplication(OutputInterface $output): void
{
$output->writeln('⚡ 优化应用...');
// 编译配置
$this->compileConfig();
// 编译路由
$this->compileRoutes();
// 优化自动加载
exec('composer dump-autoload --optimize --no-dev');
$output->writeln('✅ 应用优化完成');
}
private function restartServices(OutputInterface $output, string $env): void
{
$output->writeln('🔄 重启服务...');
if ($env === 'production') {
// 重启PHP-FPM
exec('sudo systemctl reload php-fpm');
// 重启Nginx
exec('sudo systemctl reload nginx');
// 重启队列工作进程
exec('sudo supervisorctl restart laravel-worker:*');
}
$output->writeln('✅ 服务重启完成');
}
private function deleteDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = "$dir/$file";
is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
}
rmdir($dir);
}
private function getPendingMigrations(): array
{
// 简化的迁移检查
return [];
}
private function runMigration(string $migration): void
{
// 简化的迁移执行
}
private function compileConfig(): void
{
// 配置编译逻辑
}
private function compileRoutes(): void
{
// 路由编译逻辑
}
}
最佳实践
1. 命令组织结构
app/
├── Commands/
│ ├── Database/
│ │ ├── MigrateCommand.php
│ │ ├── SeedCommand.php
│ │ └── BackupCommand.php
│ ├── Cache/
│ │ ├── ClearCommand.php
│ │ └── WarmupCommand.php
│ ├── Queue/
│ │ ├── WorkCommand.php
│ │ └── FailedCommand.php
│ └── Deploy/
│ ├── DeployCommand.php
│ └── RollbackCommand.php
2. 命令基类封装
php
use Core\Command\Command;
use Symfony\Component\Console\Command\Command as BaseCommand;
use Symfony\Component\Console\Output\OutputInterface;
abstract class AppCommand extends BaseCommand
{
protected function info(OutputInterface $output, string $message): void
{
$output->writeln("ℹ️ {$message}");
}
protected function success(OutputInterface $output, string $message): void
{
$output->writeln("✅ {$message}");
}
protected function warning(OutputInterface $output, string $message): void
{
$output->writeln("⚠️ {$message}");
}
protected function error(OutputInterface $output, string $message): void
{
$output->writeln("❌ {$message}");
}
protected function progress(OutputInterface $output, string $message): void
{
$output->writeln("🚀 {$message}");
}
}
3. 错误处理和日志
php
use Core\Command\Command;
use Psr\Log\LoggerInterface;
#[Command]
class RobustCommand extends AppCommand
{
protected static $defaultName = 'app:robust';
public function __construct(private LoggerInterface $logger)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$this->progress($output, '开始执行命令...');
// 命令逻辑
$this->doWork($output);
$this->success($output, '命令执行成功');
return Command::SUCCESS;
} catch (\Exception $e) {
$this->error($output, "命令执行失败: " . $e->getMessage());
// 记录错误日志
$this->logger->error('Command execution failed', [
'command' => static::$defaultName,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return Command::FAILURE;
}
}
private function doWork(OutputInterface $output): void
{
// 实际工作逻辑
}
}
注意事项
- 信号处理:长时间运行的命令应该正确处理SIGTERM和SIGINT信号
- 内存管理:避免内存泄漏,特别是在循环处理大量数据时
- 错误处理:提供详细的错误信息和适当的退出代码
- 日志记录:记录重要操作和错误信息
- 权限检查:执行文件操作前检查相应权限
开发建议
- 使用描述性的命令名称和参数
- 提供详细的帮助信息和使用示例
- 实现适当的进度显示和用户反馈
- 支持详细模式和静默模式
- 编写单元测试来验证命令逻辑
DuxLite 的命令行工具系统为您提供了强大的自动化能力,通过编写自定义命令,您可以高效地执行各种维护、部署和管理任务。