无需 Web 服务器,PHP 也能构建强大的命令行工具
对于长期编写 PHP 的开发者而言,脑海中的运行模型通常类似这样:
浏览器 → Web 服务器(Apache/Nginx)→ PHP → HTML 响应
这个叙事已经深入人心,以至于在许多人看来,PHP 几乎等同于“Web 开发”。开发者编写脚本,将其放入 public/ 或 htdocs/,配置虚拟主机,并习惯通过 HTTP 来运行 PHP。
关键转折在于:PHP 完全可以脱离 Web 服务器运行。终端就足以完成执行过程,而且它的能力会令人意外。
本文将介绍把 PHP 当作通用脚本语言使用时的形态,类似 Python 或 Node。文章会构建几个实用的 CLI 工具,讨论这种方式适合哪些场景,并说明为什么这个“非 Web PHP”世界比第一印象有趣得多。
不通过 Web 服务器运行 PHP
先从核心概念开始:
运行 PHP 不需要 Web 服务器。
机器上只需要安装 PHP 解释器。
如果已经安装 PHP,可以在终端中尝试:
php -v如果它输出版本字符串,就可以继续。现在创建一个名为 hello.php 的文件:
<?php
echo "Hello from the command line!\n";运行它:
php hello.php这就直接运行了一个 PHP 脚本,整个过程绕过了 HTTP 请求。PHP 直接读取文件并执行,就像任何其他脚本语言一样。
其底层原因在于 PHP 支持多种 SAPI(Server API,服务器 API),其中之一就是 CLI SAPI,即命令行接口版本。它专门面向命令行场景,用于执行 CLI 脚本;Web 请求处理由其他 SAPI 承担。
这个事实一旦被看见,思维方式会发生变化:
PHP 是一个可以用于各种任务的通用解释器。
PHP 在命令行中的行为方式
从命令行运行 PHP 时,运行环境与通过 Web 服务器运行时有所不同。
Web 环境中的一些熟悉输入在 CLI 中通常不可用:
$_GET、$_POST、$_COOKIE$_SERVER['REQUEST_METHOD']或$_SERVER['HTTP_HOST']- HTTP 请求与响应
取而代之的是更接近 Unix 的环境:
- 标准输入 / 输出(
STDIN、STDOUT、STDERR) - 命令行参数(
$argv、$argc) - 在许多环境中会使用不同的
php.ini配置文件(通常表现为php.ini与php-cli.ini的差异)
来看一个使用 CLI 特性的极小示例。
示例 一个简单的问候脚本
创建 greet.php:
<?php
// $argv is an array of command-line arguments
// $argv[0] is the script name itself
// $argv[1], $argv[2], ... are the parameters
if ($argc < 2) {
fwrite(STDERR, "Usage: php greet.php <name>\n");
exit(1);
}
$name = $argv[1];
echo "Hello, {$name}!\n";运行它:
php greet.php Alice
# Output: Hello, Alice!这里做了几件事:
- 从
$argv(参数数组)读取输入 - 输出到
STDOUT - 将错误输出到
STDERR - 失败时返回非零退出码(
exit(1)),这是标准 CLI 行为
此时,PHP 的行为更接近 Bash 或 Python,已经脱离传统 CMS 执行层的单一形象。
为什么这件事非常有趣
乍看之下,这似乎只是一个小技巧。“不错,可以脱离服务器运行 PHP 脚本。其意义何在?”
但它比这更有意思。它至少会从三个方向改变对 PHP 的理解。
1. 可以在 HTTP 之外复用 Web 应用逻辑
如果已有 Laravel、Symfony 或自定义 PHP 应用,通常已经具备:
- 验证规则
- 领域逻辑,例如计费规则、内容规则
- 数据库访问与模型
- 用于发送邮件、访问 API 等操作的服务
在命令行中运行 PHP 时,可以初始化同一套代码库,并在绕过 HTTP 入口的情况下执行任务,例如:
- 队列 Worker
- cron 任务
- 批量导入 / 导出脚本
- 维护命令
这些逻辑可以保留在 PHP 中,并通过共享代码复用。
2. 可以把 PHP 用于 DevOps 与自动化
一旦接受 PHP 是一种通用脚本语言,它就可以加入自动化工具箱:
- 文件系统操作
- 调用 API
- 解析日志
- 转换 CSV 或 JSON 数据
- 生成报告
如果团队的主要语言是 PHP,这种方式也会降低团队成员参与自动化工具开发的门槛。成员编写部署脚本或自动化工具时,可以继续使用熟悉的语言。
3. 它会促使开发者更深入理解 PHP 运行时
脱离 Web 服务器后,PHP 的实际运行方式会变得更清晰:
- 请求生命周期脱离 HTTP 绑定;它就是一个进程。
- 开发者会开始思考长时间运行的脚本。
- 内存泄漏、资源管理与优雅关闭会变得重要。
这种更深入的理解会反过来提升 Web 开发能力,因为 PHP 会呈现为一个清晰的进程模型,取代对 Apache 背后神秘组件的模糊印象。
不使用 Web 服务器运行 PHP 的实用场景
下面通过一些具体场景来说明。
1. 快速自动化脚本
假设某个文件夹中有一批 .log 文件,现在需要找出所有包含 ERROR 的行,并写入 errors.txt。
当然可以使用 grep。但如果需要更高级的处理,例如解析时间戳、按错误码分组等,可以编写一个轻量级 PHP 脚本:
<?php
// parse-logs.php
$inputDir = $argv[1] ?? null;
$outputFile = $argv[2] ?? 'errors.txt';
if (!$inputDir || !is_dir($inputDir)) {
fwrite(STDERR, "Usage: php parse-logs.php <log-directory> [output-file]\n");
exit(1);
}
$handle = fopen($outputFile, 'w');
foreach (scandir($inputDir) as $file) {
if (!str_ends_with($file, '.log')) {
continue;
}
$path = $inputDir . DIRECTORY_SEPARATOR . $file;
$lines = file($path);
foreach ($lines as $line) {
if (str_contains($line, 'ERROR')) {
fwrite($handle, $file . ': ' . $line);
}
}
}
fclose($handle);
echo "Done! Errors written to {$outputFile}\n";运行方式如下:
php parse-logs.php /var/log/myapp这时 PHP 就成为了一个日志处理工具。
2. cron 任务与定时任务
cron 最适合执行这样的命令:
php /path/to/scripts/send-daily-report.php在 send-daily-report.php 中,可以:
- 通过 PDO 连接数据库
- 生成昨日活动摘要
- 直接发送邮件,或通过邮件服务商 API 发送
这种方式通常比为了定时任务去访问某个“隐藏 HTTP 端点”更清晰。
3. 后台 Worker 与消费者
队列无处不在:
- 处理图片上传
- 发送通知
- 运行高开销计算任务
一种常见模式如下:
- Web 应用将任务入队,例如写入 Redis、RabbitMQ、SQS 等。
- 一个长时间运行的 PHP 脚本作为 Worker 持续消费并处理任务。
伪代码示例:
<?php
// worker.php (very simplified, no real Redis code)
while (true) {
$job = get_next_job_from_queue(); // you implement this
if ($job) {
try {
handle_job($job);
} catch (Throwable $e) {
log_error($e);
}
} else {
// No job? Sleep briefly to avoid CPU burning
usleep(200000); // 0.2 seconds
}
}虽然 PHP 常被认为适合短生命周期的 Web 请求,但这种模式完全有效,并且已在生产环境中使用。关键在于谨慎管理内存和资源。
4. 开发者工具与脚手架
可以构建内部工具:
- “创建新模块”的脚本
- “生成样板代码”的脚本
- 项目初始化命令,例如创建配置文件、填充数据
这些工具通常会:
- 提示用户输入
- 操作文件与目录
- 运行 shell 命令
下面是一个极小的脚手架脚本思路:
<?php
// make-module.php
$moduleName = $argv[1] ?? null;
if (!$moduleName) {
fwrite(STDERR, "Usage: php make-module.php <ModuleName>\n");
exit(1);
}
$baseDir = __DIR__ . '/modules/' . $moduleName;
if (is_dir($baseDir)) {
fwrite(STDERR, "Module {$moduleName} already exists.\n");
exit(1);
}
mkdir($baseDir, 0777, true);
file_put_contents($baseDir . '/index.php', "<?php\n\n// {$moduleName} module entry point\n");
echo "Module {$moduleName} created at {$baseDir}\n";用 PHP 构建一个真正的命令行应用
现在从玩具脚本进入更接近真实 CLI 工具的示例。
下面构建一个简单的任务管理器:
php tasks.php add "Buy milk"
php tasks.php list
php tasks.php done 2为了简化,任务会存储在 JSON 文件中。
第 1 步 基础结构
创建 tasks.php:
<?php
const STORAGE_FILE = __DIR__ . '/tasks.json';
function loadTasks(): array
{
if (!file_exists(STORAGE_FILE)) {
return [];
}
$json = file_get_contents(STORAGE_FILE);
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
function saveTasks(array $tasks): void
{
file_put_contents(STORAGE_FILE, json_encode($tasks, JSON_PRETTY_PRINT));
}
function printUsage(): void
{
echo <<<USAGE
Usage:
php tasks.php list
php tasks.php add "<description>"
php tasks.php done <id>
USAGE;
}这段代码提供了:
- 一个存储文件(
tasks.json) - 用于加载 / 保存任务的辅助函数
- 一个打印使用说明的函数
第 2 步 处理命令
扩展 tasks.php:
<?php
const STORAGE_FILE = __DIR__ . '/tasks.json';
function loadTasks(): array
{
if (!file_exists(STORAGE_FILE)) {
return [];
}
$json = file_get_contents(STORAGE_FILE);
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
function saveTasks(array $tasks): void
{
file_put_contents(STORAGE_FILE, json_encode($tasks, JSON_PRETTY_PRINT));
}
function printUsage(): void
{
echo <<<USAGE
Usage:
php tasks.php list
php tasks.php add "<description>"
php tasks.php done <id>
USAGE;
}
function listTasks(array $tasks): void
{
if (empty($tasks)) {
echo "No tasks yet. 🎉\n";
return;
}
foreach ($tasks as $id => $task) {
$status = $task['done'] ? '[x]' : '[ ]';
echo sprintf("%d. %s %s\n", $id, $status, $task['description']);
}
}
function addTask(array &$tasks, string $description): void
{
$tasks[] = [
'description' => $description,
'done' => false,
];
echo "Task added: {$description}\n";
}
function markDone(array &$tasks, int $id): void
{
if (!isset($tasks[$id])) {
echo "Task {$id} not found.\n";
return;
}
$tasks[$id]['done'] = true;
echo "Task {$id} marked as done.\n";
}
// ---------- CLI entry point ----------
$argvCopy = $argv;
array_shift($argvCopy); // remove script name
$command = $argvCopy[0] ?? null;
$tasks = loadTasks();
switch ($command) {
case 'list':
listTasks($tasks);
break;
case 'add':
$description = $argvCopy[1] ?? null;
if (!$description) {
echo "Please provide a task description.\n";
printUsage();
exit(1);
}
addTask($tasks, $description);
saveTasks($tasks);
break;
case 'done':
$id = isset($argvCopy[1]) ? (int)$argvCopy[1] : null;
if ($id === null) {
echo "Please provide a task ID.\n";
printUsage();
exit(1);
}
markDone($tasks, $id);
saveTasks($tasks);
break;
default:
printUsage();
exit(1);
}现在可以执行:
php tasks.php add "Write PHP article"
php tasks.php add "Drink coffee"
php tasks.php list
php tasks.php done 0
php tasks.php list这样就构建了一个虽小但真实的应用:
- 它可以持久化状态
- 它具备命令与子命令
- 它的行为类似任何其他 CLI 工具
整个过程依然脱离 Web 服务器。
继续进阶 使用库构建 CLI 应用
上面的示例刻意保持极简。在真实应用中,通常还会需要:
- 彩色输出
- 参数与选项解析
- 帮助信息
- 子命令
- 交互式提示
这些能力可以手写,不过完全可以使用成熟库。以下工具正是为此而生:
symfony/console- Laravel Zero
- Robo
- 各类框架 CLI,例如 Laravel Artisan
例如使用 symfony/console 时,通常会:
- 通过 Composer 引入:
composer require symfony/console创建一个 console 脚本,用于启动 Console 应用并注册命令。
将命令写成 PHP 类。
这些命令会获得:
- 自动
--help - 样式化输出
- 输入校验
- 嵌套命令,例如
app:user:create
这里的重点在于认识到:PHP CLI 生态已经相当成熟。把 PHP 当作 CLI 优先语言使用,是一种受支持且主流的模式。
不通过 HTTP 请求访问 API 与数据库
脱离 Web 服务器运行依然可以访问网络。
CLI PHP 脚本仍然可以:
- 调用 REST API
- 访问数据库
- 向队列发布消息
- 从云存储读取数据
示例 从 API 获取 JSON
下面是一个使用 file_get_contents 的极简示例:
<?php
// fetch-user.php
$userId = $argv[1] ?? null;
if (!$userId) {
fwrite(STDERR, "Usage: php fetch-user.php <user-id>\n");
exit(1);
}
$url = "https://jsonplaceholder.typicode.com/users/{$userId}";
$json = @file_get_contents($url);
if ($json === false) {
fwrite(STDERR, "Failed to fetch user.\n");
exit(1);
}
$data = json_decode($json, true);
if (!is_array($data)) {
fwrite(STDERR, "Invalid JSON received.\n");
exit(1);
}
echo "Name: {$data['name']}\n";
echo "Email: {$data['email']}\n";可以在终端中运行它,将其作为某个自动化流水线的一部分。
示例 使用 PDO 访问数据库
连接数据库的方式与 Web 代码完全一致:
<?php
// count-users.php
$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';
$user = 'myuser';
$pass = 'mypassword';
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$stmt = $pdo->query('SELECT COUNT(*) FROM users');
$count = (int) $stmt->fetchColumn();
echo "Total users: {$count}\n";把它接入 cron 任务后,一个“报表系统”可能只需几个 PHP 脚本。
Web PHP 与 CLI PHP 的重要差异
不通过 Web 服务器运行 PHP 会让人感到熟悉,但仍有几个关键差异需要记住。
1. 不同的超全局变量
在 Web 环境中,代码可能依赖:
$_GET、$_POST、$_REQUEST$_COOKIE、$_SESSION$_SERVER['REQUEST_URI']等
在 CLI 中:
- 这些变量通常为空或无关。
- 应使用
$argv、$argc、STDIN。
因此,在复用 Web 代码时,可能需要重构逻辑,使其减少对 HTTP 特定全局变量的依赖。一个好的模式是将领域逻辑与“交付机制”(Web/CLI/API)分离。
2. 不同配置 php.ini 与 php-cli.ini
许多系统会为 CLI 提供独立配置:
- 启用 / 禁用的扩展
- 内存限制
- 错误显示设置
这很有用,因为:
- CLI 中可能需要更详细的错误输出。
- CLI 脚本可能需要允许更长执行时间。
如果出现“浏览器中可以运行,CLI 中却异常”或反向情况,就应检查这类配置差异。
3. 长时间运行脚本与内存
Web 请求通常生命周期很短。请求结束后,PHP 进程终止,内存被释放。
CLI 脚本,尤其是 Worker,可能运行数小时甚至数天。这意味着:
- 必须更谨慎地处理内存泄漏,例如长期保留的大数组。
- 应确保数据库连接被复用或被正确关闭。
- 可以考虑通过 supervisor、systemd 等工具定期重启 Worker。
一种简单的内存使用量查看方式:
echo "Memory usage: " . memory_get_usage(true) . " bytes\n";在循环中执行重型处理时,这一点尤为重要。
4. 手动管理请求生命周期
在 Web 框架中,许多生命周期由框架自动处理:
- 中间件
- 路由
- 控制器
- 响应
在 CLI 中,需要自行负责。这既自由,也意味着需要更多设计:
- 设计自己的“入口点”。
- 决定如何处理失败与重试。
- 自行实现结构,或借助库完成。
连接 Web 与 CLI 两个世界
一种非常好的模式是让 Web 入口与 CLI 入口共享同一个框架和领域代码。
例如:
- 在 Laravel 中,
php artisan本质上就是应用的 CLI 前端。可以注册复用模型、服务等内容的命令。 - 在 Symfony 中,
bin/console也类似,它会启动 Symfony kernel。 - 在自定义应用中,可以创建一个同时供 Web 与 CLI 使用的
bootstrap.php:
// bootstrap.php
<?php
require __DIR__ . '/vendor/autoload.php';
// setup container, config, database, etc.
$container = MyApp\Bootstrap::createContainer();
return $container;然后在 CLI 脚本中:
// cli-script.php
<?php
/** @var Psr\Container\ContainerInterface $container */
$container = require __DIR__ . '/bootstrap.php';
$reportService = $container->get(MyApp\Service\ReportGenerator::class);
$reportService->sendDailyReport();通过这种方式:
- 业务逻辑位于可复用的类中。
- Web 控制器与 CLI 脚本只是适配器。
- Web 服务器只是触发 PHP 代码的一种方式。
什么时候适合这样使用 PHP
明确地说,Bash、Python、Go 等工具依然有各自适合的位置。但 PHP CLI 在一些特定情况下非常合适:
- 团队的主要语言是 PHP,并希望所有成员都能参与自动化工具开发。
- 已经拥有丰富的 PHP 代码库,并希望在 HTTP 之外复用逻辑。
- 希望“应用”和“任务”使用同一种语言,降低多语言切换成本。
也有一些场景会更偏向其他方案:
- 需要单个静态二进制文件,例如发布一个依赖极少的小型 CLI。
- 面向边缘设备,需要极小运行时占用。
- 团队与生态已经深度投入另一种脚本语言。
重点在于认识到,PHP 比刻板印象中更通用。
PHP 远不止 Web 服务器背后的执行器
完全不使用 Web 服务器运行 PHP,初看像一个小把戏。但真正尝试之后,会发现几个事实:
- PHP 是完全有能力胜任命令行任务的脚本语言。
- 可以编写真实 CLI 工具:任务管理器、日志解析器、自动化脚本、部署辅助工具。
- 可以将 Web 应用的领域逻辑复用于 cron 任务、Worker 与后台任务。
- 可以更深入理解 PHP 在 HTTP 之外的运行方式。
如果此前从未这样使用过 PHP,可以从一个简单练习开始:
- 编写一个只通过 CLI 完成实用工作的 PHP 脚本,例如解析文件、调用 API、重命名文件。
- 使用
$argv添加参数。 - 将它变成一个可复用工具,放入自己的
bin/目录。
熟悉之后,可以继续下一步:
- 引入
symfony/console或其他 CLI 框架。 - 注册几个连接现有应用逻辑的命令。
- 让 PHP 同时承担项目中的 Web 与“非 Web”任务。
PHP 脱离 Web 服务器后依然可用,而且是一种非常顺手的工具构建方式,这正是它有趣之处。