PHP 老项目维护实录:从代码冗余到功能迭代的破局之道
做 PHP 开发这六年,一半时间都在跟老项目打交道 —— 有的是十年前用原生 PHP 写的 CMS 系统,有的是 ThinkPHP3.2 遗留的电商后台,代码里没注释、函数嵌套三层、全局变量满天飞,每次接手都像拆盲盒。最近刚完成一个老会员系统的功能迭代,不仅加了新的积分兑换模块,还把祖传的 “面条代码” 捋顺了,这里把踩过的坑和总结的方法分享出来,给同样在维护老项目的同行做个参考。
一、先 “摸清家底”:老项目维护的第一步不是改代码
刚接手上个会员项目时,我犯了个急功近利的错 —— 看到登录接口响应慢,上来就想优化 SQL,结果改了半天才发现,代码里藏着三个不同版本的 “用户状态判断” 函数,有的查 session,有的查 cookie,还有的直接读数据库,改了一个根本没用。后来才明白,维护老项目的核心不是 “上来就改”,而是先理清现有逻辑。
我花了三天时间做了两件事:
- 画 “代码地图”:把核心模块(登录、会员中心、积分管理)的调用关系理清楚,比如用户登录后,login.php会调用user_check.php的checkUserStatus()函数,再通过auth.php的setAuthCookie()设置登录态,把这些关系用思维导图画出来,后续改代码就知道 “动一处会影响哪”;
- 标注 “危险区域”:在代码里用注释标出不能轻易动的部分,比如老项目里有个getUserBalance()函数,不仅算会员余额,还偷偷处理了积分过期逻辑,这种 “一个函数干两件事” 的地方,标注上 “修改需同步检查积分模块”,避免踩坑。
举个例子,最初看登录代码时,发现一段奇怪的逻辑:
// 老项目里的登录逻辑片段
function userLogin($username, $password) {
// 查用户信息
$user = mysql_query("select * from user where username='{$username}'");
$userInfo = mysql_fetch_assoc($user);
// 密码验证
if (md5($password) != $userInfo['password']) {
return false;
}
// 这里突然跳转到积分检查?
checkUserPoint($userInfo['id']);
// 设置session
$_SESSION['uid'] = $userInfo['id'];
return true;
}
如果没提前梳理,直接把checkUserPoint()删掉想优化性能,就会导致用户登录后积分不更新 —— 这就是 “摸清家底” 的重要性,老项目的坑往往藏在这种 “不合常理” 的逻辑里。
二、代码重构:别追求 “一步到位”,用 “渐进式修改” 降低风险
老项目最让人头疼的是代码冗余,比如之前的会员项目里,“获取用户信息” 的逻辑在登录、会员中心、订单提交三个地方各写了一遍,有的用mysql_query,有的用自定义封装的db_get(),改个用户表字段要改三处代码。
但重构不能 “一刀切”,直接把所有旧代码换成新框架不现实,反而容易出线上问题。我用的是 “渐进式修改”:先封装通用函数,再逐步替换旧代码。
比如先写一个统一的getUserInfo()函数,放在common/new_db.php里(单独建个新文件,避免污染旧代码):
// 新封装的用户信息获取函数
function getUserInfo($uid, $fields = 'id, username, balance, point') {
// 用PDO替换旧的mysql_query,避免SQL注入
global $pdo;
$sql = "SELECT {$fields} FROM user WHERE id = :uid LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':uid', $uid, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
// 兼容旧代码:如果没查到数据,返回空数组而不是false
return $result ?: [];
}
然后先在新开发的 “积分兑换” 模块里用这个新函数,跑一段时间没问题后,再慢慢把登录模块的旧逻辑换成新函数,每次只改一处,改完后用 Postman 测接口,确保功能正常。
另外,老项目里的全局变量特别多,比如$GLOBALS['user_level_name']这种,我会把它们逐步换成常量或类属性:
// 用常量替换全局变量
define('USER_LEVEL_NORMAL', '普通会员');
define('USER_LEVEL_VIP', 'VIP会员');
define('USER_LEVEL_SVIP', '超级会员');
// 后续代码里直接用常量,不用再依赖全局变量
function getUserLevelName($level) {
$levelMap = [
1 => USER_LEVEL_NORMAL,
2 => USER_LEVEL_VIP,
3 => USER_LEVEL_SVIP
];
return $levelMap[$level] ?? USER_LEVEL_NORMAL;
}
这样改的好处是,每次修改影响范围小,即使出问题也能快速定位到最近改的代码。
三、功能迭代:在老项目里加新功能,要做好 “兼容” 和 “隔离”
这次维护的核心需求是加 “积分兑换商品” 模块,老项目里虽然有积分字段,但没做过相关逻辑,直接加功能很容易和旧代码冲突。我总结了两个关键点:新功能尽量隔离,和旧逻辑交互做好兼容。
首先是 “隔离”:新模块的代码单独放在point_exchange/目录下,包括控制器(ExchangeController.php)、模型(ExchangeModel.php)、视图(exchange_list.html),和老项目的目录结构区分开,避免混在一起不好维护。
然后是 “兼容”:新模块需要调用老项目的用户信息、积分修改逻辑,这时候不能直接改旧代码,而是写 “适配层”。比如老项目里修改积分用的是updateUserPoint($uid, $point)(直接传最终积分),而新模块需要 “增加 / 减少积分”,就写个适配函数:
// 新模块和老项目的适配函数
function adjustUserPoint($uid, $adjustNum) {
// 先调用新封装的getUserInfo获取当前积分
$userInfo = getUserInfo($uid, 'point');
$currentPoint = $userInfo['point'] ?: 0;
// 计算新积分(注意:adjustNum可以是负数,代表扣减)
$newPoint = max(0, $currentPoint + $adjustNum);
// 调用老项目的updateUserPoint函数,实现兼容
return updateUserPoint($uid, $newPoint);
}
// 新模块里兑换商品扣积分时,直接用适配函数
function exchangeGoods($uid, $goodsId) {
// 1. 获取商品所需积分
$goodsPoint = getExchangeGoodsPoint($goodsId);
// 2. 扣减用户积分(用适配函数,兼容老逻辑)
$adjustResult = adjustUserPoint($uid, -$goodsPoint);
if (!$adjustResult) {
return ['code' => -1, 'msg' => '积分不足'];
}
// 3. 记录兑换日志(新逻辑,单独写)
recordExchangeLog($uid, $goodsId, $goodsPoint);
return ['code' => 0, 'msg' => '兑换成功'];
}
这样既用到了老项目的核心逻辑,又不用改旧代码,新功能出问题也只会影响point_exchange/目录下的代码,排查起来很方便。
另外,老项目的 PHP 版本可能比较低(我这个项目是 PHP 5.6),新功能里要避免用高版本特性,比如array_column在 PHP 5.5 以上才支持,null coalesce运算符(??)要 PHP 7.0 以上,这些都要提前查手册,避免线上报错。
四、上线前必做:老项目的 “踩坑” 检查清单
老项目上线最怕出意外,我每次上线前都会过一遍检查清单,避免低级错误:
- 版本兼容性检查:用php -v确认线上环境的 PHP 版本,比如本地用 PHP 7.2 写的代码,线上是 PHP 5.6,int类型声明会报错,要把function getGoods(int $id)改成function getGoods($id);
- 数据库字段检查:老项目的数据库表可能没加索引,新功能里如果有频繁查询的字段(比如exchange_log表的uid字段),要提前加索引,避免上线后数据库慢查询;
- 旧逻辑依赖检查:比如新功能里用到了$_SESSION['uid'],要确认老项目的登录逻辑会设置这个 session,不会出现 “新功能里获取不到 uid” 的情况;
- 备份!备份!备份!:上线前一定要备份数据库和代码,比如用mysqldump备份用户表和积分表,用tar打包整个项目目录,出问题能快速回滚。
这次上线前,我就发现线上环境的 PHP 版本是 5.6,而新写的getUserInfo()函数里用了PDO::PARAM_INT,虽然没问题,但fetch(PDO::FETCH_ASSOC)在 5.6 里正常,之前遇到过有的老环境 PDO 配置不对,会返回对象而不是数组,所以提前用var_dump测了返回格式,避免后续逻辑报错。
总结:老项目维护,耐心比技术更重要
很多 PHP 开发者觉得维护老项目 “没技术含量”,不如写新项目有意思,但其实老项目里藏着很多实战经验 —— 怎么兼容旧逻辑、怎么在不破坏现有功能的前提下加新代码、怎么排查祖传代码里的 bug,这些都是写新项目学不到的。
这次维护的会员项目,前后花了一个月,没用到什么 “高大上” 的技术,就是慢慢梳理逻辑、逐步重构代码、细心做兼容,但最终不仅顺利上线了积分兑换功能,还让后续改代码的效率提高了一倍。
最后想跟同行说:维护老项目不用急,一步一步来,把每个坑都记下来,下次遇到类似问题就能少走弯路。你们在维护 PHP 老项目时,遇到过最头疼的问题是什么?欢迎在评论区交流,咱们一起想办法解决!
更多推荐

所有评论(0)