CatchAdmin PHP 后台管理框架 Logo CatchAdmin

无需 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,可以在终端中尝试:

bash
php -v

如果它输出版本字符串,就可以继续。现在创建一个名为 hello.php 的文件:

php
<?php
echo "Hello from the command line!\n";

运行它:

bash
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 的环境:

  • 标准输入 / 输出(STDINSTDOUTSTDERR
  • 命令行参数($argv$argc
  • 在许多环境中会使用不同的 php.ini 配置文件(通常表现为 php.iniphp-cli.ini 的差异)

来看一个使用 CLI 特性的极小示例。

示例 一个简单的问候脚本

创建 greet.php

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";

运行它:

bash
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
<?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";

运行方式如下:

bash
php parse-logs.php /var/log/myapp

这时 PHP 就成为了一个日志处理工具。

2. cron 任务与定时任务

cron 最适合执行这样的命令:

bash
php /path/to/scripts/send-daily-report.php

send-daily-report.php 中,可以:

  • 通过 PDO 连接数据库
  • 生成昨日活动摘要
  • 直接发送邮件,或通过邮件服务商 API 发送

这种方式通常比为了定时任务去访问某个“隐藏 HTTP 端点”更清晰。

3. 后台 Worker 与消费者

队列无处不在:

  • 处理图片上传
  • 发送通知
  • 运行高开销计算任务

一种常见模式如下:

  1. Web 应用将任务入队,例如写入 Redis、RabbitMQ、SQS 等。
  2. 一个长时间运行的 PHP 脚本作为 Worker 持续消费并处理任务。

伪代码示例:

php
<?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
<?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 工具的示例。

下面构建一个简单的任务管理器:

bash
php tasks.php add "Buy milk"
php tasks.php list
php tasks.php done 2

为了简化,任务会存储在 JSON 文件中。

第 1 步 基础结构

创建 tasks.php

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
<?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);
}

现在可以执行:

bash
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 时,通常会:

  1. 通过 Composer 引入:
bash
composer require symfony/console
  1. 创建一个 console 脚本,用于启动 Console 应用并注册命令。

  2. 将命令写成 PHP 类。

这些命令会获得:

  • 自动 --help
  • 样式化输出
  • 输入校验
  • 嵌套命令,例如 app:user:create

这里的重点在于认识到:PHP CLI 生态已经相当成熟。把 PHP 当作 CLI 优先语言使用,是一种受支持且主流的模式。

不通过 HTTP 请求访问 API 与数据库

脱离 Web 服务器运行依然可以访问网络。

CLI PHP 脚本仍然可以:

  • 调用 REST API
  • 访问数据库
  • 向队列发布消息
  • 从云存储读取数据

示例 从 API 获取 JSON

下面是一个使用 file_get_contents 的极简示例:

php
<?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
<?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$argcSTDIN

因此,在复用 Web 代码时,可能需要重构逻辑,使其减少对 HTTP 特定全局变量的依赖。一个好的模式是将领域逻辑与“交付机制”(Web/CLI/API)分离。

2. 不同配置 php.ini 与 php-cli.ini

许多系统会为 CLI 提供独立配置:

  • 启用 / 禁用的扩展
  • 内存限制
  • 错误显示设置

这很有用,因为:

  • CLI 中可能需要更详细的错误输出。
  • CLI 脚本可能需要允许更长执行时间。

如果出现“浏览器中可以运行,CLI 中却异常”或反向情况,就应检查这类配置差异。

3. 长时间运行脚本与内存

Web 请求通常生命周期很短。请求结束后,PHP 进程终止,内存被释放。

CLI 脚本,尤其是 Worker,可能运行数小时甚至数天。这意味着:

  • 必须更谨慎地处理内存泄漏,例如长期保留的大数组。
  • 应确保数据库连接被复用或被正确关闭。
  • 可以考虑通过 supervisor、systemd 等工具定期重启 Worker。

一种简单的内存使用量查看方式:

php
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
php
// bootstrap.php
<?php
require __DIR__ . '/vendor/autoload.php';

// setup container, config, database, etc.
$container = MyApp\Bootstrap::createContainer();

return $container;

然后在 CLI 脚本中:

php
// 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,可以从一个简单练习开始:

  1. 编写一个只通过 CLI 完成实用工作的 PHP 脚本,例如解析文件、调用 API、重命名文件。
  2. 使用 $argv 添加参数。
  3. 将它变成一个可复用工具,放入自己的 bin/ 目录。

熟悉之后,可以继续下一步:

  1. 引入 symfony/console 或其他 CLI 框架。
  2. 注册几个连接现有应用逻辑的命令。
  3. 让 PHP 同时承担项目中的 Web 与“非 Web”任务。

PHP 脱离 Web 服务器后依然可用,而且是一种非常顺手的工具构建方式,这正是它有趣之处。

本作品采用《CC 协议》,转载必须注明作者和本文链接