不用 Web 服务器也能跑 PHP?这事比你想的有意思

如果你写了一段时间 PHP,脑子里大概是这个画面:

浏览器 → Web 服务器(Apache/Nginx)→ PHP → 返回 HTML

这条路径实在太经典了,以至于很多人心里,PHP 就等于 Web 开发。写个脚本,扔到 public/htdocs/ 目录,配个虚拟主机,然后通过 HTTP 访问——好像这就是运行 PHP 的唯一方式。

但其实不是这样的。PHP 可以完全脱离 Web 服务器运行

不需要 Apache,不需要 Nginx,甚至不需要浏览器。就在你的终端里,直接运行。而且,这样用起来还挺强大的。

这篇文章会聊聊,当你把 PHP 当作通用脚本语言(就像 Python 或 Node 那样)来用时,会发生什么。我们会写一些实用的命令行工具,讨论什么时候适合这么干,以及为什么这个"非 Web 的 PHP 世界"其实比听起来有趣得多。

等等,PHP 不用 Web 服务器?

核心观点很简单:

运行 PHP 不需要 Web 服务器,只需要装个 PHP 解释器。

如果你机器上有 PHP,在终端试一下:

bash
php -v

看到版本号了?那就能用。现在创建一个 hello.php

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

然后运行它:

bash
php hello.php

就这么简单。你刚才直接运行了一个 PHP 脚本,全程没有 HTTP 请求,没有 Apache,没有 Nginx,没有任何 Web 服务器。PHP 就像其他脚本语言一样,直接读文件、执行代码。

底层的原理是这样的:PHP 有不同的 SAPI(Server API),其中一个叫 CLI SAPI(Command Line Interface),专门为命令行设计,完全不依赖 Web 服务器。

这个发现可能看起来很显而易见,但它会让你的认知发生转变:

PHP 不只是"Web 应用背后的那个东西",它是一个完整的、可以做任何事情的通用解释器。

命令行环境下 PHP 长什么样

在命令行运行 PHP,环境跟 Web 服务器下完全不一样。

首先,那些熟悉的东西不见了:

  • 没有 $_GET$_POST$_COOKIE
  • 没有 $_SERVER['REQUEST_METHOD']$_SERVER['HTTP_HOST']
  • 根本就没有 HTTP 请求和响应

取而代之的是更"Unix"的环境:

  • 标准输入/输出(STDIN、STDOUT、STDERR)
  • 命令行参数($argv$argc
  • 不同的 php.ini 配置(很多系统会有单独的 php-cli.ini)

来看个实际的例子。

一个简单的问候脚本

创建 greet.php

php
<?php
// $argv 是命令行参数数组
// $argv[0] 是脚本名
// $argv[1]、$argv[2]... 是参数
if ($argc < 2) {
    fwrite(STDERR, "用法: php greet.php <name>\n");
    exit(1);
}

$name = $argv[1];
echo "Hello, {$name}!\n";

运行:

bash
php greet.php Alice
# 输出: 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 定时任务
  • 批量导入/导出脚本
  • 维护命令

不用把这些逻辑用另一门语言(Python、Bash等)重写一遍,全都用 PHP,代码可以共享。

2. 把 PHP 用于 DevOps 和自动化

一旦你接受 PHP 是个通用脚本语言,它就可以加入你的"自动化工具箱":

  • 文件系统操作
  • 调用 API
  • 解析日志
  • 转换 CSV 或 JSON 数据
  • 生成报告

如果你团队的主力语言是 PHP,这还能提高大家的参与度。不用为了写个部署脚本或自动化工具就切换语言。

3. 迫使你更深入理解 PHP 的运行时

不用 Web 服务器的工作方式,会暴露 PHP 实际上是怎么运行的:

  • 请求生命周期不再绑定 HTTP;它就是个进程
  • 你开始考虑长时间运行的脚本
  • 你会关心内存泄漏、资源管理、优雅关闭

这种更深的理解会反馈到你的 Web 开发技能上,因为你现在把 PHP 更多地看作一个进程,而不是"Apache背后那个神秘的东西"。

实际使用场景

说完理论,来看几个具体的例子。

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, "用法: 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 "完成!错误已写入 {$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(简化版,没真正的 Redis 代码)
while (true) {
    $job = get_next_job_from_queue(); // 你自己实现
    if ($job) {
        try {
            handle_job($job);
        } catch (Throwable $e) {
            log_error($e);
        }
    } else {
        // 没任务?短暂休眠避免 CPU 空转
        usleep(200000); // 0.2 秒
    }
}

虽然 PHP 以短生命周期 Web 请求闻名,但这种模式完全有效,生产环境在用。关键是仔细管理内存和资源。

4. 开发工具和脚手架

可以构建内部工具:

  • "创建新模块"脚本
  • "生成样板代码"脚本
  • 项目初始化命令(创建配置文件、数据填充等)

这些工具通常:

  • 提示用户输入
  • 操作文件和目录
  • 运行 shell 命令

小型脚手架脚本示例:

php
<?php
// make-module.php
$moduleName = $argv[1] ?? null;

if (!$moduleName) {
    fwrite(STDERR, "用法: php make-module.php <ModuleName>\n");
    exit(1);
}

$baseDir = __DIR__ . '/modules/' . $moduleName;

if (is_dir($baseDir)) {
    fwrite(STDERR, "模块 {$moduleName} 已存在\n");
    exit(1);
}

mkdir($baseDir, 0777, true);
file_put_contents($baseDir . '/index.php', "<?php\n\n// {$moduleName} 模块入口\n");

echo "模块 {$moduleName} 已创建于 {$baseDir}\n";

构建真正的命令行应用

从玩具脚本到真正的 CLI 工具。

我们来构建一个简单的任务管理器:

bash
php tasks.php add "买牛奶"
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
用法:
  php tasks.php list
  php tasks.php add "<描述>"
  php tasks.php done <id>
USAGE;
}

提供了:

  • 存储文件(tasks.json
  • 加载/保存任务的辅助函数
  • 打印用法说明的函数

第 2 步:处理命令

继续扩展 tasks.php

php
<?php
// ... 前面的代码 ...

function listTasks(array $tasks): void
{
    if (empty($tasks)) {
        echo "还没有任务 🎉\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 "任务已添加: {$description}\n";
}

function markDone(array &$tasks, int $id): void
{
    if (!isset($tasks[$id])) {
        echo "任务 {$id} 不存在\n";
        return;
    }
    $tasks[$id]['done'] = true;
    echo "任务 {$id} 已标记为完成\n";
}

// ---------- CLI 入口 ----------
$argvCopy = $argv;
array_shift($argvCopy); // 去掉脚本名
$command = $argvCopy[0] ?? null;

$tasks = loadTasks();

switch ($command) {
    case 'list':
        listTasks($tasks);
        break;
        
    case 'add':
        $description = $argvCopy[1] ?? null;
        if (!$description) {
            echo "请提供任务描述\n";
            printUsage();
            exit(1);
        }
        addTask($tasks, $description);
        saveTasks($tasks);
        break;
        
    case 'done':
        $id = isset($argvCopy[1]) ? (int)$argvCopy[1] : null;
        if ($id === null) {
            echo "请提供任务 ID\n";
            printUsage();
            exit(1);
        }
        markDone($tasks, $id);
        saveTasks($tasks);
        break;
        
    default:
        printUsage();
        exit(1);
}

现在可以:

bash
php tasks.php add "写 PHP 文章"
php tasks.php add "喝咖啡"
php tasks.php list
php tasks.php done 0
php tasks.php list

你刚构建了一个小但真实的应用:

  • 持久化状态
  • 有命令和子命令
  • 表现得像其他任何 CLI 工具

重点是,完全不需要 Web 服务器。

进阶:使用库(symfony/console、Laravel Zero 等)

上面的例子故意极简。实际应用中通常需要:

  • 彩色输出
  • 参数和选项解析
  • 帮助信息
  • 子命令
  • 交互式提示

可以手写这些,但没必要。有专门的库:

  • symfony/console
  • Laravel Zero
  • Robo
  • 各框架的 CLI(Laravel Artisan 等)

比如用 symfony/console

  1. 通过 Composer 安装:
bash
composer require symfony/console
  1. 创建控制台脚本,启动 Console 应用并注册命令

  2. 把命令写成 PHP 类

你的命令会自动获得:

  • 自动生成 --help
  • 样式化输出
  • 输入验证
  • 嵌套命令(app:user:create 等)

重点不是记住这些 API,而是认识到:**PHP CLI 生态很成熟。**把 PHP 当 CLI 优先语言不是 hack,是主流的、被支持的模式。

不通过 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, "用法: 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, "获取用户失败\n");
    exit(1);
}

$data = json_decode($json, true);
if (!is_array($data)) {
    fwrite(STDERR, "收到无效 JSON\n");
    exit(1);
}

echo "姓名: {$data['name']}\n";
echo "邮箱: {$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 "总用户数: {$count}\n";

挂到 cron job,你的"报表系统"就是几个 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 vs php-cli.ini)

很多系统对 CLI 有单独配置:

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

好处是:

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

如果"浏览器里能跑"但 CLI 不行(反之亦然),留意这点。

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

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

CLI 脚本,特别是 Worker,可能运行几小时或几天。这意味着:

  • 必须更小心内存泄漏(如永不清理的大数组)
  • 应该确保数据库连接被复用或正确关闭
  • 可能需要通过 supervisor 定期重启 Worker(如 supervisord、systemd)

查看内存的简单方法:

php
echo "内存使用: " . memory_get_usage(true) . " bytes\n";

循环做大量处理时这很重要。

4. 没有"自动"的请求生命周期

Web 框架里,很多生命周期自动处理:

  • 中间件
  • 路由
  • 控制器
  • 响应

CLI 里,你自己掌控。既自由又多一点工作:

  • 你设计自己的"入口点"
  • 你决定如何处理失败和重试
  • 你实现自己的结构或依赖库

桥接 Web 和 CLI 世界

一个很好的模式是在 Web 和 CLI 入口之间共享同样的框架和领域代码。

比如:

  • Laravelphp artisan 就是应用的 CLI 前端,可以注册命令复用模型、服务等
  • Symfonybin/console 类似——启动 Symfony 内核的 CLI
  • 自定义应用:可以创建 bootstrap.php 供两者使用:
php
// bootstrap.php
<?php
require __DIR__ . '/vendor/autoload.php';

// 设置容器、配置、数据库等
$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 之外复用逻辑
喜欢用一门语言搞定"应用"和"任务",不想折腾多种语言

相反,可能不选 PHP 的情况: ❌ 需要单个静态二进制(如无依赖分发的小 CLI)
❌ 需要极小运行时占用的边缘设备
❌ 现有团队/生态重度投资于另一种脚本语言

这不是竞争,而是认识到 PHP 比它的刻板印象更通用。

结论:PHP 不只是"Web 服务器背后的东西"

不用 Web 服务器运行 PHP 乍一听像个噱头,但试过之后会发现:

  1. PHP 是命令行的合格脚本语言
  2. 可以写真正的 CLI 工具:任务管理器、日志解析器、自动化脚本、部署助手
  3. 可以复用 Web 应用的领域逻辑用于 cron、Worker、后台任务
  4. 更深入理解 PHP 如何运行,超越 HTTP

如果从没这样用过 PHP,试试这个简单挑战:

  • 写个小 PHP 脚本,纯 CLI 干点实用的事(解析文件、调 API、重命名文件)
  • $argv 加参数
  • 把它变成可复用工具,放到你的 bin/ 目录

熟悉之后,进一步:

  • 引入 symfony/console 或其他 CLI 框架
  • 注册几个连接现有应用逻辑的命令
  • 让 PHP 同时处理 Web 和"非 Web"生活

你可能会发现,不用 Web 服务器的 PHP 不只是可行——它实际上是构建工具的愉快方式。这才是真正有意思的地方。

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