背景
接手了一个 PHP 营销站点。文件结构是典型的”上古 PHP”风格:
├── config.php
├── index.php
├── integrations.php
├── policy.php
├── tor.php
├── includes/
│ ├── nav.php
│ └── footer.php
├── lang/
├── styles.css
└── script.js
所有文件平铺在根目录,config.php 用 require 引入,_t() 函数全局定义,CSS/JS 混在 PHP 目录中。没有命名空间、没有自动加载、没有依赖管理。
需要新功能(Stripe 支付、定价页面),直接在根目录加 PHP 文件就行的时代过去了。但引入 Laravel 或 Symfony 又像用牛刀杀鸡——迁移成本高,现有功能可能被破坏。
目标是:渐进式改造,每一步都可逆,不中断现有页面。
第一步:Composer + PSR-4 自动加载
这是投入产出比最高的一步。创建 composer.json,加上 PSR-4 映射:
{
"require": {
"vlucas/phpdotenv": "^5.6",
"stripe/stripe-php": "^16"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
把环境配置抽到 src/Config.php:
namespace App;
class Config
{
public static function string(string $key, string $default = ''): string
{
return (string)($_ENV[$key] ?? $default);
}
public static function stripeSecretKey(): string
{
return static::string('STRIPE_SECRET_KEY');
}
}
修改 config.php——加三行,改不动的保留:
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
// 以下代码完全不动
session_start();
$lang = ...
$translations = require __DIR__ . "/lang/$lang.php";
function _t($key) { ... }
关键是 safeLoad() 而不是 load()——.env 文件不存在时不崩溃,开发环境友好。
第二步:public/ 入口隔离
把可公开访问的文件迁入 public/,不可公开的留在上级:
├── public/
│ ├── index.php
│ ├── integrations.php
│ ├── styles.css
│ └── script.js
├── src/
├── lang/
├── templates/
├── vendor/
├── config.php
└── .env
同时修改 nginx 根目录指向 public/:
root /var/www/html/public;
这一步风险最低——只是文件搬家和 nginx 配置调整,不涉及业务逻辑。用了 git mv 保留文件历史,回滚也只需改回 nginx 配置。
第三步:视图层分离
原有 includes/ 改名 templates/,提取页面内容区域为独立模板文件:
templates/
├── nav.php
├── footer.php
├── home.php ← 原 index.php 的 <main> 内容
├── integrations.php
├── policy.php
└── tor.php
入口文件瘦身为控制器:
// public/index.php
<?php require __DIR__ . '/../config.php'; ?>
<!DOCTYPE html>
<html>
<head>
<title><?= _t('home_page_title') ?></title>
<link rel="stylesheet" href="assets/styles.css">
</head>
<body>
<?php require __DIR__ . '/../templates/nav.php'; ?>
<?php require __DIR__ . '/../templates/home.php'; ?>
<?php require __DIR__ . '/../templates/footer.php'; ?>
<script src="assets/script.js"></script>
</body>
</html>
提取边界很清楚:<main> 内的内容属于模板,<head> 和 <script> 留在入口。每个页面的 <head> 差异较大(OG 标签、JSON-LD 结构),不适合强行抽象 layout。
第四步:加入新功能(Stripe 支付)
有了以上结构,加新功能变成纯增量工作:
src/Payment/Checkout.php ← Stripe 服务类
public/create-session.php ← 创建支付会话
public/webhook.php ← 支付回调
public/success.php ← 成功页
public/cancel.php ← 取消页
每个新文件职责单一。create-session.php 只需要 20 行:
<?php
require __DIR__ . '/../config.php';
$checkout = new \App\Payment\Checkout();
$result = $checkout->createSession($planId, $successUrl, $cancelUrl);
echo json_encode($result);
没有路由注册、没有依赖注入配置、没有服务提供者。够用,且一眼能看出逻辑。
第五步:知识库生成脚本
为了给 AI 客服提供数据,需要把网站内容(定价、功能、FAQ)提取为结构化文档。写一个 Python 脚本,直接解析 PHP 翻译文件:
import re
def parse_php_array(path):
with open(path, encoding='utf-8') as f:
content = f.read()
result = {}
for m in re.finditer(r"'([^']+)'\s*=>\s*'((?:[^'\\]|\\.)*)'", content):
key, value = m.group(1), m.group(2)
result[key] = value.replace("\\'", "'")
return result
脚本输出双语 markdown 文件,同时被 RAG 系统的 watcher 自动检测并索引。
为什么不直接上框架
| 因素 | Laravel | 本项目方案 |
|---|---|---|
| 迁移成本 | 需重写全部路由和控制器 | 零,现有页面不改 |
| 学习曲线 | 需熟悉 Artisan/ORM/Blade | 无,纯 PHP |
| 构建体积 | ~30MB | ~5MB(含 vendor) |
| 后续可扩展 | 丰富 | 够用,可随时升级 |
关键在于:这个站点不会演变为复杂的后台系统。它的本质是内容展示 + 支付转化,真正的业务逻辑在另一个独立应用中。为这样的站点上框架是过度工程。
成果
改造后的项目结构:
├── public/
│ ├── index.php
│ ├── integrations.php
│ ├── pricing.php
│ ├── success.php / cancel.php
│ ├── create-session.php
│ ├── webhook.php
│ └── assets/
│ ├── styles.css
│ ├── chat.css / chat.js
│ └── logo.svg
├── templates/
│ ├── nav.php / footer.php
│ ├── home.php
│ ├── integrations.php
│ ├── pricing.php
│ └── policy.php / tor.php
├── src/
│ ├── Config.php
│ └── Payment/
│ └── Checkout.php
├── lang/
├── config.php
├── composer.json
└── .env
所有现有页面保持 200 OK,新增的 Stripe 支付和 AI 客服零侵入接入。
小结
PHP 项目的工程化改造不一定非要上框架。Composer + PSR-4 自动加载提供了代码组织能力,public/ 隔离保障了安全,视图模板消除了页面重复。
这套模式适用于大量”内容展示为主、少量交互功能”的 PHP 站点——用最小成本换取长期可维护性。当有一天复杂度真的超出了这套模式的容纳能力时,再考虑框架也不迟。