不用 Web 服务器也能跑 PHP?这事比你想的有意思
如果你写了一段时间 PHP,脑子里大概是这个画面:
浏览器 → Web 服务器(Apache/Nginx)→ PHP → 返回 HTML
这条路径实在太经典了,以至于很多人心里,PHP 就等于 Web 开发。写个脚本,扔到 public/ 或 htdocs/ 目录,配个虚拟主机,然后通过 HTTP 访问------好像这就是运行 PHP 的唯一方式。
但其实不是这样的。PHP 可以完全脱离 Web 服务器运行。
不需要 Apache,不需要 Nginx,甚至不需要浏览器。就在你的终端里,直接运行。而且,这样用起来还挺强大的。
这篇文章会聊聊,当你把 PHP 当作通用脚本语言(就像 Python 或 Node 那样)来用时,会发生什么。我们会写一些实用的命令行工具,讨论什么时候适合这么干,以及为什么这个"非 Web 的 PHP 世界"其实比听起来有趣得多。
原文链接 不用 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 / 消费者
队列无处不在:
- 处理图片上传
- 发通知
- 跑重计算
常见模式:
- Web 应用把任务入队(Redis、RabbitMQ、SQS 等)
- 一个长时间运行的 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:
- 通过 Composer 安装:
bash
composer require symfony/console
-
创建控制台脚本,启动 Console 应用并注册命令
-
把命令写成 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、$argc、STDIN
如果复用 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 入口之间共享同样的框架和领域代码。
比如:
- Laravel :
php artisan就是应用的 CLI 前端,可以注册命令复用模型、服务等 - Symfony :
bin/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 乍一听像个噱头,但试过之后会发现:
- PHP 是命令行的合格脚本语言
- 可以写真正的 CLI 工具:任务管理器、日志解析器、自动化脚本、部署助手
- 可以复用 Web 应用的领域逻辑用于 cron、Worker、后台任务
- 更深入理解 PHP 如何运行,超越 HTTP
如果从没这样用过 PHP,试试这个简单挑战:
- 写个小 PHP 脚本,纯 CLI 干点实用的事(解析文件、调 API、重命名文件)
- 用
$argv加参数 - 把它变成可复用工具,放到你的
bin/目录
熟悉之后,进一步:
- 引入
symfony/console或其他 CLI 框架 - 注册几个连接现有应用逻辑的命令
- 让 PHP 同时处理 Web 和"非 Web"生活
你可能会发现,不用 Web 服务器的 PHP 不只是可行------它实际上是构建工具的愉快方式。这才是真正有意思的地方。