控制台与命令行

Viswoole 基于 Symfony Console 组件构建了命令行工具,提供服务控制、代码优化、配置发布等常用命令,并支持自定义命令扩展。

基本用法

bash
# 入口文件
./viswoole

# 查看所有可用命令
./viswoole list

# 查看命令帮助
./viswoole help server:start
./viswoole help --command server:start

内置命令

服务控制命令

server:start - 启动服务

bash
# 启动 HTTP 服务(前台运行)
./viswoole server:start

# 指定守护进程模式
./viswoole server:start -d

# 强制启动(忽略已有进程)
./viswoole server:start -f

# 启动指定服务(如 http、websocket)
./viswoole server:start websocket

server:close - 关闭服务

bash
# 关闭默认服务(http)
./viswoole server:close

# 关闭指定服务
./viswoole server:close websocket

server:reload - 重载服务

bash
# 平滑重载(热更新 Worker 进程)
./viswoole server:reload

# 只重载 Task Worker
./viswoole server:reload --task

# 强制重启(关闭所有服务进程后重新启动)
./viswoole server:reload --force

# 强制重启并以守护进程模式运行
./viswoole server:reload --force -d

reload vs reload --force: reload 平滑重载不会断开已有连接;reload --force 会关闭所有服务进程并重新启动,会短暂中断服务。框架没有独立的 restart 命令,强制重启请使用 server:reload --force


优化命令

optimize:facade - 优化门面缓存

bash
# 优化指定门面(namespace 为必填参数,需传入 Facade 类的完全限定名称)
./viswoole optimize:facade "Viswoole\Core\Facade\Task"
./viswoole optimize:facade "Viswoole\Cache\Facade\Cache"

门面优化会将 Facade 类的方法映射关系以 @method 注解形式写入到 Facade 类文件中,便于 IDE 识别并提供代码提示,减少运行时的反射开销。


发布命令

vendor:publish - 发布配置

bash
# 扫描所有依赖包中 extra.viswoole.configs 声明的配置文件并发布到项目根目录
./viswoole vendor:publish

# 强制覆盖已有文件
./viswoole vendor:publish --force

该命令会读取 vendor/composer/installed.json,扫描各依赖包 extra.viswoole.configs 字段声明的文件或目录,按原目录结构复制到项目根目录。


发现命令

service:discover - 服务发现

bash
# 扫描并发现所有注册的服务提供者
./viswoole service:discover

command:discover - 命令发现

bash
# 扫描依赖包中声明的命令并生成注册文件
./viswoole command:discover

该命令会读取 vendor/composer/installed.json,扫描各依赖包 extra.viswoole.commands 字段声明的命令类,并生成 vendor/commands.php 注册文件供框架自动加载。


自定义命令

创建命令类

继承 Symfony Console 的 Command 基类,并使用 #[AsCommand] 属性声明命令名称和描述:

php
namespace App\Console\Commands;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'user:create',
    description: '创建新用户'
)]
class UserCreateCommand extends Command
{
    /**
     * 定义参数和选项
     */
    protected function configure(): void
    {
        $this
            // 必填参数
            ->addArgument('name', InputArgument::REQUIRED, '用户名')
            ->addArgument('email', InputArgument::REQUIRED, '邮箱地址')

            // 可选参数
            ->addArgument('role', InputArgument::OPTIONAL, '角色', 'user')

            // 选项(--flag 形式)
            ->addOption('password', 'p', InputOption::VALUE_OPTIONAL, '密码')
            ->addOption('active', null, InputOption::VALUE_NONE, '是否激活')
            ->addOption('send-email', null, InputOption::VALUE_NONE, '发送欢迎邮件');
    }

    /**
     * 执行命令
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $name = $input->getArgument('name');
        $email = $input->getArgument('email');
        $role = $input->getArgument('role');
        $password = $input->getOption('password') ?? bin2hex(random_bytes(8));
        $isActive = $input->getOption('active');
        $sendEmail = $input->getOption('send-email');

        // 业务逻辑...
        $user = User::create([
            'name' => $name,
            'email' => $email,
            'password' => password_hash($password, PASSWORD_BCRYPT),
            'role' => $role,
            'is_active' => $isActive,
        ]);

        // 输出结果(使用 SymfonyStyle)
        $io->success("用户创建成功: ID={$user->id}");
        $io->text("<comment>初始密码: {$password}</comment>");

        if ($sendEmail) {
            // 发送邮件...
            $io->info('欢迎邮件已发送');
        }

        return Command::SUCCESS;  // 返回状态码
    }
}

注册命令

方式一:配置文件注册

config/app.phpcommands 键中注册命令类:

php
// config/app.php
return [
    // 其他配置...

    // 自定义命令注册
    'commands' => [
        \App\Console\Commands\UserCreateCommand::class,
        \App\Console\Commands\GenerateReportCommand::class,
    ],
];

方式二:依赖包自动发现

依赖包可通过 composer.jsonextra.viswoole.commands 字段声明命令类,框架启动时会自动加载,也可手动执行以下命令重新生成注册文件:

bash
./viswoole command:discover

输出方法

框架基于 Symfony Console,推荐在 execute() 中使用 SymfonyStyle 进行格式化输出:

php
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);

    // 成功输出(绿色)
    $io->success('操作成功');

    // 错误输出(红色)
    $io->error('发生错误');

    // 警告输出(黄色)
    $io->warning('注意事项');

    // 普通文本
    $io->text('普通文本');

    // 注释输出(灰色)
    $io->text('<comment>注释文本</comment>');

    // 表格输出
    $io->table(
        ['ID', '姓名', '邮箱'],
        [
            [1, '张三', 'zhangsan@example.com'],
            [2, '李四', 'lisi@example.com'],
        ]
    );

    // 列表输出
    $io->listing([
        '步骤一: 准备数据',
        '步骤二: 处理逻辑',
        '步骤三: 输出结果',
    ]);

    // 进度条
    $io->progressStart(100);
    for ($i = 0; $i < 100; $i++) {
        usleep(10000);  // 模拟工作
        $io->progressAdvance();
    }
    $io->progressFinish();

    return Command::SUCCESS;
}

交互式输入

SymfonyStyle 也提供了一系列交互式输入方法:

php
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);

    // 确认提示
    if (!$io->confirm('确定要删除吗?', false)) {
        $io->warning('操作已取消');
        return Command::INVALID;
    }

    // 文本输入
    $name = $io->ask('请输入用户名', '默认值');

    // 密码输入(隐藏显示)
    $password = $io->askHidden('请输入密码');

    // 选择输入
    $role = $io->choice('选择用户角色', ['admin', 'editor', 'user'], 'user');

    // 多选
    $permissions = $io->choice(
        '选择权限(多个用逗号分隔)',
        ['read', 'write', 'delete', 'admin'],
        'read',
        true  // 允许多选
    );

    return Command::SUCCESS;
}

调用其他命令

在一个命令中调用另一个命令,可通过 Application::find() 获取命令实例后传入新的输入输出执行:

php
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);

    // 调用其他命令
    $command = $this->getApplication()->find('optimize:facade');
    $returnCode = $command->run(
        new \Symfony\Component\Console\Input\ArrayInput([
            'namespace' => 'Viswoole\Core\Facade\Task',
        ]),
        $output
    );

    $io->text("命令返回码: {$returnCode}");

    return Command::SUCCESS;
}

命令调度(定时任务)

框架内置命令行工具未提供定时任务调度功能。如需实现命令的定时执行,可借助系统 crontab 或 Swoole 的定时器(Swoole\Timer)自行编排,例如:

bash
# 使用系统 crontab 每天凌晨执行命令
0 0 * * * cd /path/to/project && ./viswoole server:reload --task

实战示例

数据库迁移命令

php
namespace App\Console\Commands;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'db:migrate', description: '执行数据库迁移')]
class MigrateCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->addOption('path', null, InputOption::VALUE_REQUIRED, '迁移文件路径')
            ->addOption('fresh', 'f', InputOption::VALUE_NONE, '清空数据库后重建');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $path = $input->getOption('path') ?? base_path('database/migrations');
        $fresh = $input->getOption('fresh');

        if ($fresh && !$io->confirm('这将清空所有数据,确定继续吗?', false)) {
            return Command::INVALID;
        }

        $files = glob($path . '*.php');
        $io->progressStart(count($files));

        foreach ($files as $file) {
            require_once $file;
            $migrationClass = basename($file, '.php');
            $migration = new $migrationClass();

            $io->text("<comment>正在执行: {$migrationClass}</comment>");
            $migration->up();

            $io->progressAdvance();
        }

        $io->progressFinish();
        $io->success('迁移完成,共执行 ' . count($files) . ' 个文件');

        return Command::SUCCESS;
    }
}

生成代码脚手架

php
namespace App\Console\Commands;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'make:controller', description: '创建新的控制器')]
class MakeControllerCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->addArgument('name', InputArgument::REQUIRED, '控制器名称')
            ->addOption('resource', 'r', InputOption::VALUE_NONE, '生成资源控制器');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $name = $input->getArgument('name');
        $resource = $input->getOption('resource');

        $stubPath = $resource
            ? base_path('stubs/controller.resource.stub')
            : base_path('stubs/controller.plain.stub');

        $content = file_get_contents($stubPath);
        $content = str_replace('{{ class }}', $name, $content);

        $filePath = app_path("Controller/{$name}.php");

        if (file_exists($filePath) && !$io->confirm("{$name} 已存在,是否覆盖?", false)) {
            return Command::INVALID;
        }

        file_put_contents($filePath, $content);

        $io->success("控制器已创建: {$filePath}");

        return Command::SUCCESS;
    }
}

命令最佳实践

1. 状态码规范

php
return Command::SUCCESS;     // 0 - 成功
return Command::FAILURE;     // 1 - 一般性失败
return Command::INVALID;     // 2 - 参数无效

2. 异常处理

php
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);

    try {
        $this->doWork();
        return Command::SUCCESS;
    } catch (ValidationException $e) {
        $io->error('验证失败: ' . $e->getMessage());
        return Command::INVALID;
    } catch (\Throwable $e) {
        $io->error('执行出错: ' . $e->getMessage());
        if ($input->getOption('verbose')) {
            $io->text('<comment>' . $e->getTraceAsString() . '</comment>');
        }
        return Command::FAILURE;
    }
}

3. 幂等性设计

命令应支持多次执行而不产生副作用:

php
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $io = new SymfonyStyle($input, $output);

    // 检查是否已执行过
    if ($this->hasAlreadyRun()) {
        $io->info('已经执行过,跳过');
        return Command::SUCCESS;
    }

    // 执行逻辑...
    $this->markAsDone();

    return Command::SUCCESS;
}