PHP 老项目改造:从一坨 index.php 到能维护的结构
这个博客本身就是个老项目。
最早的时候它是一个 4000 行的 index.php:路由、数据库、模板、工具函数全在一个文件里,用 if/elseif 判断 $_GET['r']。
八年下来它还在跑,但每次改东西我都要滚很久才能找到位置。去年花了几个周末重构,记录一下过程。
一、先别急着分层
很多重构教程第一步就教你上框架、上 Composer、上命名空间。
对一个还在跑的老项目,这是最危险的做法。你会在重构的过程中引入一堆新 bug,然后分不清是重构引入的还是原来就有的。
我做的第一件事是:给关键路径加日志和回归测试,不是重构。
二、第一步:把「公共函数」抽出去
原文件里有 60 多个函数:e()(转义)、human_time()、cover_of()、pub_where()……
这些函数没有副作用,抽出来最安全。
# 拆分顺序:从「被最多地方调用」的函数开始
grep -o "function [a-z_]*" index.php | sort | uniq -c | sort -rn | head
我把它们按用途分到三个文件:
lib.php # 公共函数(转义、时间、URL、设置)
theme.php # 布局:layout_header / layout_footer / 各种组件
member.php # 会员中心(独立路由)
现在 index.php 只剩路由和一个 require。
关键点:拆分时不改函数体。 哪怕函数写得再烂,先原样搬过去。改函数体留到后面单独做,这样出问题时能确定是「搬家」还是「改动」。
三、第二步:给路由加一个统一入口
原来是:
if ($r === '/') home_page();
elseif ($r === '/archive') archive_page();
elseif (preg_match('#^/post/([a-z0-9-]+)$#', $r, $m)) post_page($m[1]);
这个结构其实没问题,改造成本低。我只做了两件事:
- 把
$r的解析统一到顶部(原来散落在各处) - 加了一个 404 兜底
$r = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$r = '/' . trim($r, '/');
不要为了「优雅」把它改成 Router 类。老项目改造的第一原则是减少同时变化的量。
四、第三步:数据库访问
原来到处都是 db()->query(...) 加字符串拼接。
// 原来
$rows = db()->query("SELECT * FROM posts WHERE category_id=" . (int)$cid)->fetchAll();
危险的不是 (int),而是有些地方忘了转。
我做的改造:
// 之后
$st = $P->prepare("SELECT * FROM posts WHERE category_id=?");
$st->execute([$cid]);
$rows = $st->fetchAll();
一次性改完不现实。我的做法是「新代码必须用 prepare,老代码遇到 bug 时顺手改」。
两年下来,高危的拼接点基本都改完了。剩下一些 ORDER BY 这种不能参数化的地方,用白名单:
$order = in_array($sort, ['created_at', 'views', 'like_count'], true) ? $sort : 'created_at';
$sql .= " ORDER BY $order DESC";
五、第四步:模板
原来是这样:
echo '<div class="post">';
echo '<h2>' . e($p['title']) . '</h2>';
echo '<p>' . e($p['summary']) . '</p>';
echo '</div>';
现在是这样:
<?php foreach ($rows as $p): ?>
<article class="post">
<h2><?= e($p['title']) ?></h2>
<p><?= e($p['summary']) ?></p>
</article>
<?php endforeach; ?>
PHP 本身就是模板语言,用 <?= ?> 短标签比 echo 拼接可读性好很多,而且不用引入 Twig 这类依赖。
注意 e() 一定不能忘。我现在的习惯是:模板里输出任何变量都套 e(),除非明确知道它是 HTML。
六、第五步:配置与密钥
原来数据库密码写在 config.php 里,而 config.php 在 web 根目录下。虽然 PHP 会执行它不会泄露源码,但这是个隐患。
现在的结构:
/www/wwwroot/blog/
├─ public/ # web 根
│ ├─ index.php
│ └─ assets/
├─ app/ # 不在 web 根下
│ ├─ lib.php
│ ├─ theme.php
│ └─ member.php
└─ config.php # 在 web 根之外
改这个要动 Nginx 的 root,风险在于「改错了全站 404」。我的做法是先 cp -a 整个目录到 blog-new,在测试域名上跑通,再切 Nginx 配置。
七、我做错的两件事
一、过早引入 Composer
我一开始装了一堆包(monolog、dotenv、parsedown),结果发现:
- 服务器上没有 composer,每次部署要手动
composer install - 依赖升级引入了不兼容问题
后来只保留了 Parsedown(一个文件,直接 require),其他都去掉了。
老项目引入依赖的收益,往往小于维护成本。
二、改了函数签名
human_time($d) 我改成 human_time($d, $short = false),以为加了默认值就兼容。但有三个地方用了 array_map('human_time', $arr),多传一个参数,行为变了。
后来我把这类改动全部改成新增函数(human_time_short()),老函数不动。
八、改造后的样子
index.php:从 4000 行到 200 行(只有路由)lib.php:公共函数与数据库辅助theme.php:布局与组件member.php:会员中心admin.php:后台(还是很大,但独立)
一共花了大概 20 个小时,分散在两个月里。中间没有停过服务。
九、如果重来
我会按这个顺序:
- 加日志(记录慢查询、错误)
- 抽公共函数(不改函数体)
- 抽模板(用
<?= ?>) - 数据库改 prepare(遇到才改)
- 配置文件移出 web 根
框架、依赖注入、单元测试这些,对一个每天几十个访问的个人站来说,收益远小于成本。
老项目能跑八年,说明它的设计在「够用」这个标准上是成立的。改造的目标是让它再跑八年,不是把它变成一个教科书项目。
评论 (0)
还没有评论,来说两句吧~
发表评论