应先用 php script.php 验证 PHP CLI 环境是否可用,因其配置文件(如 /etc/php/8.3/cli/php.ini)与 Web 模式独立,需检查扩展、内存限制、时区等设置,并用 php --ini、php -m 和最小验证脚本确认。

直接用 php script.php 运行,别急着加 shebang 或 chmod +x —— 大部分本地调试失败,都是因为跳过了这一步验证环境是否真能跑通。
怎么确认 PHP CLI 环境可用
不是看 php -v 有输出就万事大吉。CLI 模式用的是独立的配置文件(比如 /etc/php/8.3/cli/php.ini),和 Web 模式完全不共享。常见问题包括:
-
extension=mbstring在 CLI 的php.ini里被注释了,导致脚本一用中文就报Call to undefined function mb_strlen() -
memory_limit默认是 128M,但处理 CSV 导入时瞬间爆内存,而 Web 模式下你可能早调高了 -
date.timezone未设置,date()返回Warning: date(): It is not safe to rely on the system's timezone settings
实操建议:
- 运行
php --ini查看 CLI 实际加载的php.ini路径 - 用
php -m确认扩展已启用(注意:CLI 和 FPM 的扩展列表可能不同) - 写个最小验证脚本:
php -r "echo date('c') . "\n"; echo extension_loaded('pdo_mysql') ? 'OK' : 'MISSING';"
参数解析:什么时候该用 getopt(),什么时候必须上 symfony/console
getopt() 适合单命令、无子命令、选项不超过 5 个的脚本(比如 php backup.php -d /data -c gzip --dry-run)。它原生支持短选项(-h)、长选项(--help)和带值参数(-f file.txt),但不处理:
立即学习“PHP免费学习笔记(深入)”;
- 参数顺序无关性(
php cmd.php --file a.json --verbose和php cmd.php --verbose --file a.json结果应一致,但getopt()不保证) - 自动帮助文本生成(得自己
echo一堆格式化文字) - 类型校验(比如把
--port=abc当成整数报错) - Windows cmd 下长选项被截断(
--config变成--confi)
一旦出现以下任一情况,立刻切到 symfony/console:
- 需要子命令(
php app.php user:create/user:delete) - 参数含必填/可选/默认值/数组(如
--tag=prod --tag=staging) - 要集成到 Laravel/Symfony 项目中复用服务容器
示例(symfony/console 注册一个带选项的命令):
protected function configure(): void
{
$this->setName('process:import')
->setDescription('Import users from CSV')
->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'CSV path')
->addOption('batch-size', null, InputOption::VALUE_OPTIONAL, 'Rows per transaction', 100);
}
输入输出必须分流:别让错误信息混进正常结果
CLI 工具常被管道调用(如 php extract.php | grep "active"),如果错误也走 echo,会污染 STDIN 流,导致下游命令解析失败。
- 正常输出用
fwrite(STDOUT, ...)或echo(CLI 下echo默认写STDOUT) - 错误/警告必须走
fwrite(STDERR, ...),例如:fwrite(STDERR, "Failed to connect: {$e->getMessage()} "); - 用户交互(如确认删除)用
fgets(STDIN),记得rtrim($input, " ")去掉换行符 - 彩色输出前先检测终端支持:
function_exists('posix_isatty') && posix_isatty(STDOUT),否则 ANSI 序列会变成乱码
退出码不能省:exit(0) 表示成功;exit(1) 是通用错误;自定义错误建议用 exit(2)(参数错误)、exit(3)(连接失败)等,方便 Shell 脚本判断。
为什么简单脚本也建议用 Composer + symfony/console
不是为了“重”,而是避免重复踩坑:
-
$argv在某些 SAPI(如 HHVM 旧版)下索引行为不一致,symfony/console封装层做了兼容 - 帮助文本自动对齐选项、描述、默认值,手写容易错位或漏空格
- 参数绑定后,
$input->getArgument('file')直接返回字符串,不用再isset($argv[1]) ? $argv[1] : null - 异常未捕获时,
symfony/console自动输出堆栈并返回非零退出码,而裸脚本可能静默失败
最小初始化只需三步:
composer require symfony/console- 新建
bin/app,写 shebang +require __DIR__.'/../vendor/autoload.php'; - 注册命令类,调用
$application->run();
真正难的从来不是“怎么写第一个命令”,而是当工具从个人脚本变成团队共用时,参数语义是否清晰、错误提示是否可操作、退出码是否可被自动化流程依赖——这些细节,symfony/console 默默帮你守住了底线。



















