本文目录导读:

在 PHP 中组织命令行脚本,我建议采用以下最佳实践架构:
基础目录结构
project/
├── bin/ # 可执行脚本入口
│ └── app.php
├── src/ # 业务逻辑代码
│ ├── Command/ # 命令类
│ ├── Service/ # 服务层
│ └── Helper/ # 辅助函数
├── config/ # 配置文件
├── vendor/ # 依赖包
└── tests/ # 测试目录
使用 Symfony Console 组件
推荐使用 symfony/console,这是最流行的 PHP CLI 框架:
// bin/app.php
#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\Console\Application;
use App\Command\HelloCommand;
$app = new Application('My CLI App', '1.0.0');
$app->add(new HelloCommand());
$app->run();
// src/Command/HelloCommand.php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class HelloCommand extends Command
{
protected function configure()
{
$this->setName('hello')
->setDescription('Say hello')
->addArgument('name', InputArgument::REQUIRED, 'Your name')
->addOption('greeting', 'g', InputOption::VALUE_OPTIONAL, 'Custom greeting', 'Hello');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getArgument('name');
$greeting = $input->getOption('greeting');
$output->writeln("<info>{$greeting}, {$name}!</info>");
return Command::SUCCESS;
}
}
轻量级脚本 - 单文件组织
对于简单任务,可以组织为:
#!/usr/bin/env php
<?php
// 手动加载
require_once __DIR__ . '/vendor/autoload.php';
// 参数解析辅助函数
function parseArgs(array $argv): array {
$args = [];
foreach (array_slice($argv, 1) as $arg) {
if (str_starts_with($arg, '--')) {
$parts = explode('=', ltrim($arg, '--'), 2);
$args[$parts[0]] = $parts[1] ?? true;
} elseif (str_starts_with($arg, '-')) {
$args[ltrim($arg, '-')] = true;
} else {
$args[] = $arg;
}
}
return $args;
}
// 主逻辑
function main(array $args): void {
// 你的业务逻辑
}
// 入口
if (PHP_SAPI === 'cli') {
$args = parseArgs($argv);
main($args);
}
编写健壮的 CLI 脚本
1 颜色输出辅助类
// src/Helper/Output.php
namespace App\Helper;
class Output {
const COLORS = [
'red' => '31',
'green' => '32',
'yellow' => '33',
'blue' => '34',
'magenta' => '35',
'cyan' => '36'
];
public static function color(string $text, string $color): string {
return "\033[" . self::COLORS[$color] . "m{$text}\033[0m";
}
public static function success(string $msg): void {
echo self::color($msg, 'green') . PHP_EOL;
}
public static function error(string $msg): void {
fwrite(STDERR, self::color($msg, 'red') . PHP_EOL);
}
}
2 进度条和交互
// 进度条示例
class ProgressBar {
private $total;
private $current = 0;
public function __construct(int $total) {
$this->total = $total;
}
public function advance(int $step = 1): void {
$this->current += $step;
$percent = round(($this->current / $this->total) * 100);
printf("\rProgress: [%s] %d%%",
str_repeat('=', $percent/2) . str_repeat(' ', 50 - $percent/2),
$percent
);
}
}
环境检测和依赖验证
// bin/app.php
<?php
// 检查运行环境
if (PHP_SAPI !== 'cli') {
die('This script can only be run from command line');
}
// 检查 PHP 版本
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
die('PHP 8.0 or higher is required');
}
// 自动加载
require __DIR__ . '/../vendor/autoload.php';
// 错误处理
set_error_handler(function($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function($e) {
fwrite(STDERR, "Unhandled exception: " . $e->getMessage() . PHP_EOL);
exit(1);
});
配置管理
// config/app.php
return [
'app_name' => 'My CLI Tool',
'version' => '1.0.0',
'debug' => getenv('APP_DEBUG') ?: false,
'database' => [
'host' => getenv('DB_HOST') ?: 'localhost',
'port' => getenv('DB_PORT') ?: 3306,
]
];
测试组织
// tests/Command/HelloCommandTest.php
namespace Tests\Command;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use App\Command\HelloCommand;
class HelloCommandTest extends TestCase {
public function testExecute() {
$application = new Application();
$application->add(new HelloCommand());
$command = $application->find('hello');
$commandTester = new CommandTester($command);
$commandTester->execute([
'command' => $command->getName(),
'name' => 'World'
]);
$output = $commandTester->getDisplay();
$this->assertStringContainsString('Hello, World!', $output);
}
}
实际使用示例
# 查看帮助 php bin/app.php list # 运行命令 php bin/app.php hello John --greeting="Hi" # 后台执行 nohup php bin/app.php long-running-task > /var/log/app.log 2>&1 & # 定时执行 0 * * * * /usr/bin/php /path/to/bin/app.php backup-database
关键最佳实践:
- 参数解析:使用专业的参数解析器(Symfony Console 或 GetOpt)
- 错误处理:使用异常和 exit codes
- 日志记录:输出到 STDOUT/STDERR,需要时才写文件
- 依赖注入:使用容器管理服务
- 环境配置:使用环境变量或配置文件
- 严格输入验证:验证所有输入参数
- 脚本权限:设置可执行权限
chmod +x bin/app.php
这样的组织方式既适合简单的单文件脚本,也适合复杂的命令行应用程序。