PHP 老项目改造:从一坨 index.php 到能维护的结构

开发 · 7 分钟阅读· 18 次阅读

这个博客本身就是个老项目。

最早的时候它是一个 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]);

这个结构其实没问题,改造成本低。我只做了两件事:

  1. 把 $r 的解析统一到顶部(原来散落在各处)
  2. 加了一个 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 个小时,分散在两个月里。中间没有停过服务。

九、如果重来

我会按这个顺序:

  1. 加日志(记录慢查询、错误)
  2. 抽公共函数(不改函数体)
  3. 抽模板(用 <?= ?>)
  4. 数据库改 prepare(遇到才改)
  5. 配置文件移出 web 根

框架、依赖注入、单元测试这些,对一个每天几十个访问的个人站来说,收益远小于成本。

老项目能跑八年,说明它的设计在「够用」这个标准上是成立的。改造的目标是让它再跑八年,不是把它变成一个教科书项目。

评论 (0)

还没有评论,来说两句吧~

发表评论

验证码

提交后需站长审核,通过后才会显示。