如果你写了一段时间 PHP,脑子里大概是这个画面:
浏览器 → Web 服务器(Apache/Nginx)→ PHP → 返回 HTML这条路径实在太经典了,以至于很多人心里,PHP 就等于 Web 开发。写个脚本,扔到 public/ 或 htdocs/ 目录,配个虚拟主机,然后通过 HTTP 访问——好像这就是运行 PHP 的唯一方式。
但其实不是这样的。PHP 可以完全脱离 Web 服务器运行。
不需要 Apache,不需要 Nginx,甚至不需要浏览器。就在你的终端里,直接运行。而且,这样用起来还挺强大的。
这篇文章会聊聊,当你把 PHP 当作通用脚本语言(就像 Python 或 Node 那样)来用时,会发生什么。我们会写一些实用的命令行工具,讨论什么时候适合这么干,以及为什么这个"非 Web 的 PHP 世界"其实比听起来有趣得多。
核心观点很简单:
运行 PHP 不需要 Web 服务器,只需要装个 PHP 解释器。
如果你机器上有 PHP,在终端试一下:
php -v看到版本号了?那就能用。现在创建一个 hello.php:
<?php
echo "Hello from the command line!\n";然后运行它:
php hello.php就这么简单。你刚才直接运行了一个 PHP 脚本,全程没有 HTTP 请求,没有 Apache,没有 Nginx,没有任何 Web 服务器。PHP 就像其他脚本语言一样,直接读文件、执行代码。
底层的原理是这样的:PHP 有不同的 SAPI(Server API),其中一个叫 CLI SAPI(Command Line Interface),专门为命令行设计,完全不依赖 Web 服务器。
这个发现可能看起来很显而易见,但它会让你的认知发生转变:
PHP 不只是"Web 应用背后的那个东西",它是一个完整的、可以做任何事情的通用解释器。
在命令行运行 PHP,环境跟 Web 服务器下完全不一样。
首先,那些熟悉的东西不见了:
$_GET、$_POST、$_COOKIE$_SERVER['REQUEST_METHOD'] 或 $_SERVER['HTTP_HOST']取而代之的是更"Unix"的环境:
$argv、$argc)来看个实际的例子。
创建 greet.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";运行:
php greet.php Alice
# 输出: Hello, Alice!在这个例子里,我们:
$argv 读取命令行参数exit(1)),这是标准的 CLI 行为到这里,PHP 表现得更像 Bash 或 Python,而不是"CMS 背后的那个东西"了。
乍一看,你可能会想:"好吧,能在终端跑 PHP 脚本了,所以呢?"
但其实这比看起来有趣。它至少从三个方面改变了你对 PHP 的认知。
如果你有一个 Laravel、Symfony 或者自己写的 PHP 应用,那你已经有了:
当你在命令行运行 PHP 时,可以启动同样的代码库,跑一些任务,完全不用通过 HTTP。比如:
不用把这些逻辑用另一门语言(Python、Bash等)重写一遍,全都用 PHP,代码可以共享。
一旦你接受 PHP 是个通用脚本语言,它就可以加入你的"自动化工具箱":
如果你团队的主力语言是 PHP,这还能提高大家的参与度。不用为了写个部署脚本或自动化工具就切换语言。
不用 Web 服务器的工作方式,会暴露 PHP 实际上是怎么运行的:
这种更深的理解会反馈到你的 Web 开发技能上,因为你现在把 PHP 更多地看作一个进程,而不是"Apache背后那个神秘的东西"。
说完理论,来看几个具体的例子。
比如你有一文件夹的 .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, "用法: 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";运行:
php parse-logs.php /var/log/myappPHP 瞬间变成日志处理工具。
Cron 最喜欢这种命令:
php /path/to/scripts/send-daily-report.php在 send-daily-report.php 里可以:
这比为了定时任务专门搞个"隐藏 HTTP 端点"清爽多了。
队列无处不在:
常见模式:
伪代码示例:
<?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 请求闻名,但这种模式完全有效,生产环境在用。关键是仔细管理内存和资源。
可以构建内部工具:
这些工具通常:
小型脚手架脚本示例:
<?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 工具。
我们来构建一个简单的任务管理器:
php tasks.php add "买牛奶"
php tasks.php list
php tasks.php done 2用 JSON 文件存储任务。
创建 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
用法:
php tasks.php list
php tasks.php add "<描述>"
php tasks.php done <id>
USAGE;
}提供了:
tasks.json)继续扩展 tasks.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);
}现在可以:
php tasks.php add "写 PHP 文章"
php tasks.php add "喝咖啡"
php tasks.php list
php tasks.php done 0
php tasks.php list你刚构建了一个小但真实的应用:
重点是,完全不需要 Web 服务器。
上面的例子故意极简。实际应用中通常需要:
可以手写这些,但没必要。有专门的库:
symfony/console比如用 symfony/console:
composer require symfony/console创建控制台脚本,启动 Console 应用并注册命令
把命令写成 PHP 类
你的命令会自动获得:
--helpapp:user:create 等)重点不是记住这些 API,而是认识到:**PHP CLI 生态很成熟。**把 PHP 当 CLI 优先语言不是 hack,是主流的、被支持的模式。
"不用 Web 服务器"不等于"不用网络"。
CLI PHP 脚本仍然可以:
用 file_get_contents 的简单例子:
<?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";可以从终端运行,作为自动化流水线的一部分。
连数据库和 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 "总用户数: {$count}\n";挂到 cron job,你的"报表系统"就是几个 PHP 脚本的事儿。
不用 Web 服务器运行 PHP 感觉熟悉,但有些关键区别要记住。
Web 上下文会用:
$_GET、$_POST、$_REQUEST$_COOKIE、$_SESSION$_SERVER['REQUEST_URI'] 等CLI 中:
$argv、$argc、STDIN如果复用 Web 代码,可能需要重构逻辑,让它不依赖 HTTP 特定的全局变量。好的模式是把领域逻辑和"交付机制"(Web/CLI/API)分开。
很多系统对 CLI 有单独配置:
好处是:
如果"浏览器里能跑"但 CLI 不行(反之亦然),留意这点。
Web 请求通常短生命周期。请求结束后,PHP 进程结束,内存释放。
CLI 脚本,特别是 Worker,可能运行几小时或几天。这意味着:
查看内存的简单方法:
echo "内存使用: " . memory_get_usage(true) . " bytes\n";循环做大量处理时这很重要。
Web 框架里,很多生命周期自动处理:
CLI 里,你自己掌控。既自由又多一点工作:
一个很好的模式是在 Web 和 CLI 入口之间共享同样的框架和领域代码。
比如:
php artisan 就是应用的 CLI 前端,可以注册命令复用模型、服务等bin/console 类似——启动 Symfony 内核的 CLIbootstrap.php 供两者使用:// bootstrap.php
<?php
require __DIR__ . '/vendor/autoload.php';
// 设置容器、配置、数据库等
$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();这样:
明确一点,我不是说你该放弃 Bash、Python 或 Go。但 PHP CLI 在几种特定情况下很出色:
✅ 团队主力语言是 PHP,希望所有人都能贡献自动化工具
✅ 已有丰富的 PHP 代码库,想在 HTTP 之外复用逻辑
✅ 喜欢用一门语言搞定"应用"和"任务",不想折腾多种语言
相反,可能不选 PHP 的情况: ❌ 需要单个静态二进制(如无依赖分发的小 CLI)
❌ 需要极小运行时占用的边缘设备
❌ 现有团队/生态重度投资于另一种脚本语言
这不是竞争,而是认识到 PHP 比它的刻板印象更通用。
不用 Web 服务器运行 PHP 乍一听像个噱头,但试过之后会发现:
如果从没这样用过 PHP,试试这个简单挑战:
$argv 加参数bin/ 目录熟悉之后,进一步:
symfony/console 或其他 CLI 框架你可能会发现,不用 Web 服务器的 PHP 不只是可行——它实际上是构建工具的愉快方式。这才是真正有意思的地方。